1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-13 15:23:49 +01:00

Merge branch 'dev' into api_size_limits

This commit is contained in:
J. Nick Koston
2025-10-05 22:29:00 -05:00
committed by GitHub
181 changed files with 4111 additions and 1841 deletions

View File

@@ -1 +1 @@
4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9 499db61c1aa55b98b6629df603a56a1ba7aff5a9a7c781a5c1552a9dcd186c08

View File

@@ -466,7 +466,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env: env:
SKIP: pylint,clang-tidy-hash SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -102,12 +102,12 @@ jobs:
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -182,13 +182,13 @@ jobs:
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr' if: matrix.registry == 'ghcr'
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Stale - name: Stale
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with: with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true remove-stale-when-updated: true

View File

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

View File

@@ -160,7 +160,6 @@ esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz esphome/components/espnow/* @jesserockz
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/ezo_pmp/* @carlos-sarmiento

View File

@@ -14,9 +14,11 @@ from typing import Protocol
import argcomplete import argcomplete
# Note: Do not import modules from esphome.components here, as this would
# cause them to be loaded before external components are processed, resulting
# in the built-in version being used instead of the external component one.
from esphome import const, writer, yaml_util from esphome import const, writer, yaml_util
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.mqtt import CONF_DISCOVER_IP
from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import ( from esphome.const import (
ALLOWED_NAME_CHARS, ALLOWED_NAME_CHARS,
@@ -240,6 +242,8 @@ def has_ota() -> bool:
def has_mqtt_ip_lookup() -> bool: def has_mqtt_ip_lookup() -> bool:
"""Check if MQTT is available and IP lookup is supported.""" """Check if MQTT is available and IP lookup is supported."""
from esphome.components.mqtt import CONF_DISCOVER_IP
if CONF_MQTT not in CORE.config: if CONF_MQTT not in CORE.config:
return False return False
# Default Enabled # Default Enabled
@@ -1155,7 +1159,9 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file(s).", nargs="+" "configuration", help="Your YAML configuration file(s).", nargs="+"
) )
parser_clean_all = subparsers.add_parser("clean-all", help="Clean all files.") parser_clean_all = subparsers.add_parser(
"clean-all", help="Clean all build and platform files."
)
parser_clean_all.add_argument( parser_clean_all.add_argument(
"configuration", help="Your YAML configuration directory.", nargs="*" "configuration", help="Your YAML configuration directory.", nargs="*"
) )

View File

@@ -12,6 +12,7 @@ from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer, ArrayInitializer,
Expression, Expression,
LineComment, LineComment,
LogStringLiteral,
MockObj, MockObj,
MockObjClass, MockObjClass,
Pvariable, Pvariable,

View File

@@ -14,6 +14,7 @@ from esphome.const import (
CONF_EVENT, CONF_EVENT,
CONF_ID, CONF_ID,
CONF_KEY, CONF_KEY,
CONF_MAX_CONNECTIONS,
CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED, CONF_ON_CLIENT_DISCONNECTED,
CONF_PASSWORD, CONF_PASSWORD,
@@ -59,6 +60,8 @@ CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services" CONF_CUSTOM_SERVICES = "custom_services"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
def validate_encryption_key(value): def validate_encryption_key(value):
@@ -158,6 +161,42 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
single=True single=True
), ),
# Connection limits to prevent memory exhaustion on resource-constrained devices
# Each connection uses ~500-1000 bytes of RAM plus system resources
# Platform defaults based on available RAM and network stack implementation:
cv.SplitDefault(
CONF_LISTEN_BACKLOG,
esp8266=1, # Limited RAM (~40KB free), LWIP raw sockets
esp32=4, # More RAM (520KB), BSD sockets
rp2040=1, # Limited RAM (264KB), LWIP raw sockets like ESP8266
bk72xx=4, # Moderate RAM, BSD-style sockets
rtl87xx=4, # Moderate RAM, BSD-style sockets
host=4, # Abundant resources
ln882x=4, # Moderate RAM
): cv.int_range(min=1, max=10),
cv.SplitDefault(
CONF_MAX_CONNECTIONS,
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
esp32=8, # 520KB RAM available
rp2040=4, # 264KB RAM but LWIP constraints
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=8, # Abundant resources
ln882x=8, # Moderate RAM
): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Platform defaults based on available RAM and typical message rates:
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
esp8266=5, # Limited RAM, need to fail fast
esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=16, # Abundant resources
ln882x=8, # Moderate RAM
): cv.int_range(min=1, max=64),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS), cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
@@ -176,6 +215,11 @@ async def to_code(config):
cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
if CONF_LISTEN_BACKLOG in config:
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
if CONF_MAX_CONNECTIONS in config:
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_SERVICES if any services are enabled # Set USE_API_SERVICES if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:

View File

@@ -116,8 +116,7 @@ void APIConnection::start() {
APIError err = this->helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
this->log_warning_(LOG_STR("Helper init failed"), err);
return; return;
} }
this->client_info_.peername = helper_->getpeername(); this->client_info_.peername = helper_->getpeername();
@@ -147,8 +146,7 @@ void APIConnection::loop() {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
this->log_socket_operation_failed_(err);
return; return;
} }
@@ -163,17 +161,13 @@ void APIConnection::loop() {
// No more data available // No more data available
break; break;
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
this->log_warning_(LOG_STR("Reading failed"), err);
return; return;
} else { } else {
this->last_traffic_ = now; this->last_traffic_ = now;
// read a packet // read a packet
if (buffer.data_len > 0) { this->read_message(buffer.data_len, buffer.type,
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->flags_.remove) if (this->flags_.remove)
return; return;
} }
@@ -205,7 +199,8 @@ void APIConnection::loop() {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(),
this->client_info_.peername.c_str());
} }
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
// Only send ping if we're not disconnecting // Only send ping if we're not disconnecting
@@ -255,7 +250,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
// close will happen on next loop // close will happen on next loop
ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
this->flags_.next_close = true; this->flags_.next_close = true;
DisconnectResponse resp; DisconnectResponse resp;
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
@@ -1385,7 +1380,7 @@ void APIConnection::complete_authentication_() {
} }
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
#endif #endif
@@ -1579,8 +1574,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
delay(0); delay(0);
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
this->log_socket_operation_failed_(err);
return false; return false;
} }
if (this->helper_->can_write_without_blocking()) if (this->helper_->can_write_without_blocking())
@@ -1599,8 +1593,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
if (err == APIError::WOULD_BLOCK) if (err == APIError::WOULD_BLOCK)
return false; return false;
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
this->log_warning_(LOG_STR("Packet write failed"), err);
return false; return false;
} }
// Do not set last_traffic_ on send // Do not set last_traffic_ on send
@@ -1609,12 +1602,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
void APIConnection::on_unauthenticated_access() { void APIConnection::on_unauthenticated_access() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
} }
#endif #endif
void APIConnection::on_no_setup_connection() { void APIConnection::on_no_setup_connection() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
} }
void APIConnection::on_fatal_error() { void APIConnection::on_fatal_error() {
this->helper_->close(); this->helper_->close();
@@ -1786,8 +1779,7 @@ void APIConnection::process_batch_() {
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf}, APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
std::span<const PacketInfo>(packet_info, packet_count)); std::span<const PacketInfo>(packet_info, packet_count));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
this->log_warning_(LOG_STR("Batch write failed"), err);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1866,12 +1858,8 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES #endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_warning_(const LogString *message, APIError err) { void APIConnection::log_warning_(const LogString *message, APIError err) {
ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message), ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(),
LOG_STR_ARG(api_error_to_logstr(err)), errno); LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}
void APIConnection::log_socket_operation_failed_(APIError err) {
this->log_warning_(LOG_STR("Socket operation failed"), err);
} }
} // namespace esphome::api } // namespace esphome::api

View File

@@ -19,14 +19,6 @@ namespace esphome::api {
struct ClientInfo { struct ClientInfo {
std::string name; // Client name from Hello message std::string name; // Client name from Hello message
std::string peername; // IP:port from socket std::string peername; // IP:port from socket
std::string get_combined_info() const {
if (name == peername) {
// Before Hello message, both are the same
return name;
}
return name + " (" + peername + ")";
}
}; };
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
@@ -278,7 +270,8 @@ class APIConnection final : public APIServerConnection {
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } const std::string &get_name() const { return this->client_info_.name; }
const std::string &get_peername() const { return this->client_info_.peername; }
protected: protected:
// Helper function to handle authentication completion // Helper function to handle authentication completion
@@ -739,8 +732,11 @@ class APIConnection final : public APIServerConnection {
// Helper function to log API errors with errno // Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err); void log_warning_(const LogString *message, APIError err);
// Specific helper for duplicated error message // Helper to handle fatal errors with logging
void log_socket_operation_failed_(APIError err); inline void fatal_error_with_log_(const LogString *message, APIError err) {
this->on_fatal_error();
this->log_warning_(message, err);
}
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -13,7 +13,8 @@ namespace esphome::api {
static const char *const TAG = "api.frame_helper"; static const char *const TAG = "api.frame_helper";
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) #define HELPER_LOG(msg, ...) \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
@@ -80,7 +81,7 @@ const LogString *api_error_to_logstr(APIError err) {
// Default implementation for loop - handles sending buffered data // Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() { APIError APIFrameHelper::loop() {
if (!this->tx_buf_.empty()) { if (this->tx_buf_count_ > 0) {
APIError err = try_send_tx_buf_(); APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; return err;
@@ -102,9 +103,20 @@ APIError APIFrameHelper::handle_socket_write_error_() {
// Helper method to buffer data from IOVs // Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) { uint16_t offset) {
SendBuffer buffer; // Check if queue is full
buffer.size = total_write_len - offset; if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
buffer.data = std::make_unique<uint8_t[]>(buffer.size); HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
this->state_ = State::FAILED;
return;
}
uint16_t buffer_size = total_write_len - offset;
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
buffer = std::make_unique<SendBuffer>(SendBuffer{
.data = std::make_unique<uint8_t[]>(buffer_size),
.size = buffer_size,
.offset = 0,
});
uint16_t to_skip = offset; uint16_t to_skip = offset;
uint16_t write_pos = 0; uint16_t write_pos = 0;
@@ -117,12 +129,15 @@ void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt,
// Include this segment (partially or fully) // Include this segment (partially or fully)
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip; const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip; uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(buffer.data.get() + write_pos, src, len); std::memcpy(buffer->data.get() + write_pos, src, len);
write_pos += len; write_pos += len;
to_skip = 0; to_skip = 0;
} }
} }
this->tx_buf_.push_back(std::move(buffer));
// Update circular buffer tracking
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_++;
} }
// This method writes data to socket or buffers it // This method writes data to socket or buffers it
@@ -140,7 +155,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
#endif #endif
// Try to send any existing buffered data first if there is any // Try to send any existing buffered data first if there is any
if (!this->tx_buf_.empty()) { if (this->tx_buf_count_ > 0) {
APIError send_result = try_send_tx_buf_(); APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it // If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) { if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
@@ -149,7 +164,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
// If there is still data in the buffer, we can't send, buffer // If there is still data in the buffer, we can't send, buffer
// the new data and return // the new data and return
if (!this->tx_buf_.empty()) { if (this->tx_buf_count_ > 0) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered return APIError::OK; // Success, data buffered
} }
@@ -177,32 +192,31 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
} }
// Common implementation for trying to send buffered data // Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method // IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
APIError APIFrameHelper::try_send_tx_buf_() { APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
bool tx_buf_empty = false; while (this->tx_buf_count_ > 0) {
while (!tx_buf_empty) {
// Get the first buffer in the queue // Get the first buffer in the queue
SendBuffer &front_buffer = this->tx_buf_.front(); SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
// Try to send the remaining data in this buffer // Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining()); ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
if (sent == -1) { if (sent == -1) {
return this->handle_socket_write_error_(); return this->handle_socket_write_error_();
} else if (sent == 0) { } else if (sent == 0) {
// Nothing sent but not an error // Nothing sent but not an error
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) { } else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
// Partially sent, update offset // Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t // Cast to ensure no overflow issues with uint16_t
front_buffer.offset += static_cast<uint16_t>(sent); front_buffer->offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
} else { } else {
// Buffer completely sent, remove it from the queue // Buffer completely sent, remove it from the queue
this->tx_buf_.pop_front(); this->tx_buf_[this->tx_buf_head_].reset();
// Update empty status for the loop condition this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
tx_buf_empty = this->tx_buf_.empty(); this->tx_buf_count_--;
// Continue loop to try sending the next buffer // Continue loop to try sending the next buffer
} }
} }

View File

@@ -1,7 +1,8 @@
#pragma once #pragma once
#include <array>
#include <cstdint> #include <cstdint>
#include <deque>
#include <limits> #include <limits>
#include <memory>
#include <span> #include <span>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -89,7 +90,7 @@ class APIFrameHelper {
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop(); virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
std::string getpeername() { return socket_->getpeername(); } std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() { APIError close() {
@@ -171,7 +172,7 @@ class APIFrameHelper {
}; };
// Containers (size varies, but typically 12+ bytes on 32-bit) // Containers (size varies, but typically 12+ bytes on 32-bit)
std::deque<SendBuffer> tx_buf_; std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<struct iovec> reusable_iovs_; std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
@@ -184,7 +185,10 @@ class APIFrameHelper {
State state_{State::INITIALIZE}; State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0}; uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0}; uint8_t frame_footer_size_{0};
// 5 bytes total, 3 bytes padding uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// 8 bytes total, 0 bytes padding
// Common initialization for both plaintext and noise protocols // Common initialization for both plaintext and noise protocols
APIError init_common_(); APIError init_common_();

View File

@@ -24,7 +24,8 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif #endif
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) #define HELPER_LOG(msg, ...) \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())

View File

@@ -18,7 +18,8 @@ namespace esphome::api {
static const char *const TAG = "api.plaintext"; static const char *const TAG = "api.plaintext";
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) #define HELPER_LOG(msg, ...) \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())

View File

@@ -87,7 +87,7 @@ void APIServer::setup() {
return; return;
} }
err = this->socket_->listen(4); err = this->socket_->listen(this->listen_backlog_);
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed(); this->mark_failed();
@@ -140,9 +140,19 @@ void APIServer::loop() {
while (true) { while (true) {
struct sockaddr_storage source_addr; struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr); socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock) if (!sock)
break; break;
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str());
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this); auto *conn = new APIConnection(std::move(sock), this);
@@ -167,7 +177,8 @@ void APIServer::loop() {
// Network is down - disconnect all clients // Network is down - disconnect all clients
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
client->on_fatal_error(); client->on_fatal_error();
ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str()); ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
client->client_info_.peername.c_str());
} }
// Continue to process and clean up the clients below // Continue to process and clean up the clients below
} }
@@ -206,8 +217,10 @@ void APIServer::loop() {
void APIServer::dump_config() { void APIServer::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"Server:\n" "Server:\n"
" Address: %s:%u", " Address: %s:%u\n"
network::get_use_address().c_str(), this->port_); " Listen backlog: %u\n"
" Max connections: %u",
network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) { if (!this->noise_ctx_->has_psk()) {

View File

@@ -44,6 +44,8 @@ class APIServer : public Component, public Controller {
void set_reboot_timeout(uint32_t reboot_timeout); void set_reboot_timeout(uint32_t reboot_timeout);
void set_batch_delay(uint16_t batch_delay); void set_batch_delay(uint16_t batch_delay);
uint16_t get_batch_delay() const { return batch_delay_; } uint16_t get_batch_delay() const { return batch_delay_; }
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections // Get reference to shared buffer for API connections
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; } std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
@@ -189,8 +191,12 @@ class APIServer : public Component, public Controller {
// Group smaller types together // Group smaller types together
uint16_t port_{6053}; uint16_t port_{6053};
uint16_t batch_delay_{100}; uint16_t batch_delay_{100};
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults
uint8_t listen_backlog_{4};
uint8_t max_connections_{8};
bool shutting_down_ = false; bool shutting_down_ = false;
// 5 bytes used, 3 bytes padding // 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>(); std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();

View File

@@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
protected: protected:
virtual void execute(Ts... x) = 0; virtual void execute(Ts... x) = 0;
template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...> type) { template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...); this->execute((get_execute_arg_value<Ts>(args[S]))...);
} }

View File

@@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All(
) )
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA), .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
esp32_ble_tracker.consume_connection_slots(1, "ble_client"), esp32_ble.consume_connection_slots(1, "ble_client"),
) )
CONF_BLE_CLIENT_ID = "ble_client_id" CONF_BLE_CLIENT_ID = "ble_client_id"

View File

@@ -42,9 +42,7 @@ def validate_connections(config):
) )
elif config[CONF_ACTIVE]: elif config[CONF_ACTIVE]:
connection_slots: int = config[CONF_CONNECTION_SLOTS] connection_slots: int = config[CONF_CONNECTION_SLOTS]
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
config
)
return { return {
**config, **config,
@@ -65,11 +63,11 @@ CONFIG_SCHEMA = cv.All(
default=DEFAULT_CONNECTION_SLOTS, default=DEFAULT_CONNECTION_SLOTS,
): cv.All( ): cv.All(
cv.positive_int, cv.positive_int,
cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS), cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
), ),
cv.Optional(CONF_CONNECTIONS): cv.All( cv.Optional(CONF_CONNECTIONS): cv.All(
cv.ensure_list(CONNECTION_SCHEMA), cv.ensure_list(CONNECTION_SCHEMA),
cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS), cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
), ),
} }
) )

View File

@@ -21,8 +21,8 @@ void Canbus::dump_config() {
} }
} }
void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const std::vector<uint8_t> &data) { const std::vector<uint8_t> &data) {
struct CanFrame can_message; struct CanFrame can_message;
uint8_t size = static_cast<uint8_t>(data.size()); uint8_t size = static_cast<uint8_t>(data.size());
@@ -45,13 +45,15 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm
ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]); ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]);
} }
if (this->send_message(&can_message) != canbus::ERROR_OK) { canbus::Error error = this->send_message(&can_message);
if (error != canbus::ERROR_OK) {
if (use_extended_id) { if (use_extended_id) {
ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed!", can_id); ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", can_id, error);
} else { } else {
ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed!", can_id); ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", can_id, error);
} }
} }
return error;
} }
void Canbus::add_trigger(CanbusTrigger *trigger) { void Canbus::add_trigger(CanbusTrigger *trigger) {

View File

@@ -70,11 +70,11 @@ class Canbus : public Component {
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override; void loop() override;
void send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const std::vector<uint8_t> &data); const std::vector<uint8_t> &data);
void send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) { canbus::Error send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
// for backwards compatibility only // for backwards compatibility only
this->send_data(can_id, use_extended_id, false, data); return this->send_data(can_id, use_extended_id, false, data);
} }
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }

View File

@@ -11,14 +11,14 @@ namespace captive_portal {
static const char *const TAG = "captive_portal"; static const char *const TAG = "captive_portal";
void CaptivePortal::handle_config(AsyncWebServerRequest *request) { void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream(F("application/json")); AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json"));
stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate")); stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate"));
#ifdef USE_ESP8266 #ifdef USE_ESP8266
stream->print(F("{\"mac\":\"")); stream->print(ESPHOME_F("{\"mac\":\""));
stream->print(get_mac_address_pretty().c_str()); stream->print(get_mac_address_pretty().c_str());
stream->print(F("\",\"name\":\"")); stream->print(ESPHOME_F("\",\"name\":\""));
stream->print(App.get_name().c_str()); stream->print(App.get_name().c_str());
stream->print(F("\",\"aps\":[{}")); stream->print(ESPHOME_F("\",\"aps\":[{}"));
#else #else
stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str());
#endif #endif
@@ -29,19 +29,19 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
// Assumes no " in ssid, possible unicode isses? // Assumes no " in ssid, possible unicode isses?
#ifdef USE_ESP8266 #ifdef USE_ESP8266
stream->print(F(",{\"ssid\":\"")); stream->print(ESPHOME_F(",{\"ssid\":\""));
stream->print(scan.get_ssid().c_str()); stream->print(scan.get_ssid().c_str());
stream->print(F("\",\"rssi\":")); stream->print(ESPHOME_F("\",\"rssi\":"));
stream->print(scan.get_rssi()); stream->print(scan.get_rssi());
stream->print(F(",\"lock\":")); stream->print(ESPHOME_F(",\"lock\":"));
stream->print(scan.get_with_auth()); stream->print(scan.get_with_auth());
stream->print(F("}")); stream->print(ESPHOME_F("}"));
#else #else
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(),
scan.get_with_auth()); scan.get_with_auth());
#endif #endif
} }
stream->print(F("]}")); stream->print(ESPHOME_F("]}"));
request->send(stream); request->send(stream);
} }
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
@@ -52,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->save_wifi_sta(ssid, psk);
wifi::global_wifi_component->start_scanning(); wifi::global_wifi_component->start_scanning();
request->redirect(F("/?save")); request->redirect(ESPHOME_F("/?save"));
} }
void CaptivePortal::setup() { void CaptivePortal::setup() {
@@ -75,7 +75,7 @@ void CaptivePortal::start() {
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
this->dns_server_ = make_unique<DNSServer>(); this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
this->dns_server_->start(53, F("*"), ip); this->dns_server_->start(53, ESPHOME_F("*"), ip);
#endif #endif
this->initialized_ = true; this->initialized_ = true;
@@ -88,10 +88,10 @@ void CaptivePortal::start() {
} }
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
if (req->url() == F("/config.json")) { if (req->url() == ESPHOME_F("/config.json")) {
this->handle_config(req); this->handle_config(req);
return; return;
} else if (req->url() == F("/wifisave")) { } else if (req->url() == ESPHOME_F("/wifisave")) {
this->handle_wifisave(req); this->handle_wifisave(req);
return; return;
} }
@@ -100,11 +100,11 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
// This includes OS captive portal detection endpoints which will trigger // This includes OS captive portal detection endpoints which will trigger
// the captive portal when they don't receive their expected responses // the captive portal when they don't receive their expected responses
#ifndef USE_ESP8266 #ifndef USE_ESP8266
auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
#else #else
auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
#endif #endif
response->addHeader(F("Content-Encoding"), F("gzip")); response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
req->send(response); req->send(response);
} }

View File

@@ -11,7 +11,7 @@ void CopyLock::setup() {
traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_assumed_state(source_->traits.get_assumed_state());
traits.set_requires_code(source_->traits.get_requires_code()); traits.set_requires_code(source_->traits.get_requires_code());
traits.set_supported_states(source_->traits.get_supported_states()); traits.set_supported_states_mask(source_->traits.get_supported_states_mask());
traits.set_supports_open(source_->traits.get_supports_open()); traits.set_supports_open(source_->traits.get_supports_open());
this->publish_state(source_->state); this->publish_state(source_->state);

View File

@@ -1,5 +1,6 @@
#include "cover.h" #include "cover.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <strings.h>
namespace esphome { namespace esphome {
namespace cover { namespace cover {

View File

@@ -197,7 +197,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
esp32.only_on_variant( esp32.only_on_variant(
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1" unsupported=[VARIANT_ESP32C2, VARIANT_ESP32C3],
msg_prefix="Wakeup from ext1",
), ),
cv.Schema( cv.Schema(
{ {
@@ -214,7 +215,13 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_TOUCH_WAKEUP): cv.All( cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
esp32.only_on_variant( esp32.only_on_variant(
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch" unsupported=[
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
],
msg_prefix="Wakeup from touch",
), ),
cv.boolean, cv.boolean,
), ),

View File

@@ -34,7 +34,7 @@ enum WakeupPinMode {
WAKEUP_PIN_MODE_INVERT_WAKEUP, WAKEUP_PIN_MODE_INVERT_WAKEUP,
}; };
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
struct Ext1Wakeup { struct Ext1Wakeup {
uint64_t mask; uint64_t mask;
esp_sleep_ext1_wakeup_mode_t wakeup_mode; esp_sleep_ext1_wakeup_mode_t wakeup_mode;
@@ -50,7 +50,7 @@ struct WakeupCauseToRunDuration {
uint32_t gpio_cause; uint32_t gpio_cause;
}; };
#endif #endif // USE_ESP32
template<typename... Ts> class EnterDeepSleepAction; template<typename... Ts> class EnterDeepSleepAction;
@@ -73,20 +73,22 @@ class DeepSleepComponent : public Component {
void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; } void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; }
void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
#endif #endif // USE_ESP32
#if defined(USE_ESP32) #if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C3) #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); void set_ext1_wakeup(Ext1Wakeup ext1_wakeup);
void set_touch_wakeup(bool touch_wakeup);
#endif #endif
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
void set_touch_wakeup(bool touch_wakeup);
#endif
// Set the duration in ms for how long the code should run before entering // Set the duration in ms for how long the code should run before entering
// deep sleep mode, according to the cause the ESP32 has woken. // deep sleep mode, according to the cause the ESP32 has woken.
void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration);
#endif #endif // USE_ESP32
/// Set a duration in ms for how long the code should run before entering deep sleep mode. /// Set a duration in ms for how long the code should run before entering deep sleep mode.
void set_run_duration(uint32_t time_ms); void set_run_duration(uint32_t time_ms);
@@ -117,13 +119,13 @@ class DeepSleepComponent : public Component {
InternalGPIOPin *wakeup_pin_; InternalGPIOPin *wakeup_pin_;
WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE};
#if !defined(USE_ESP32_VARIANT_ESP32C3) #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
optional<Ext1Wakeup> ext1_wakeup_; optional<Ext1Wakeup> ext1_wakeup_;
#endif #endif
optional<bool> touch_wakeup_; optional<bool> touch_wakeup_;
optional<WakeupCauseToRunDuration> wakeup_cause_to_run_duration_; optional<WakeupCauseToRunDuration> wakeup_cause_to_run_duration_;
#endif #endif // USE_ESP32
optional<uint32_t> run_duration_; optional<uint32_t> run_duration_;
bool next_enter_deep_sleep_{false}; bool next_enter_deep_sleep_{false};
bool prevent_{false}; bool prevent_{false};

View File

@@ -7,6 +7,26 @@
namespace esphome { namespace esphome {
namespace deep_sleep { namespace deep_sleep {
// Deep Sleep feature support matrix for ESP32 variants:
//
// | Variant | ext0 | ext1 | Touch | GPIO wakeup |
// |-----------|------|------|-------|-------------|
// | ESP32 | ✓ | ✓ | ✓ | |
// | ESP32-S2 | ✓ | ✓ | ✓ | |
// | ESP32-S3 | ✓ | ✓ | ✓ | |
// | ESP32-C2 | | | | ✓ |
// | ESP32-C3 | | | | ✓ |
// | ESP32-C5 | | (✓) | | (✓) |
// | ESP32-C6 | | ✓ | | ✓ |
// | ESP32-H2 | | ✓ | | |
//
// Notes:
// - (✓) = Supported by hardware but not yet implemented in ESPHome
// - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup)
// - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup)
// - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup)
// - GPIO wakeup: GPIO wakeup for non-RTC pins (esp_deep_sleep_enable_gpio_wakeup)
static const char *const TAG = "deep_sleep"; static const char *const TAG = "deep_sleep";
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
@@ -30,13 +50,13 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
this->wakeup_pin_mode_ = wakeup_pin_mode; this->wakeup_pin_mode_ = wakeup_pin_mode;
} }
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
#if !defined(USE_ESP32_VARIANT_ESP32H2)
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
#endif #endif
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
#endif #endif
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
@@ -72,9 +92,13 @@ bool DeepSleepComponent::prepare_to_sleep_() {
} }
void DeepSleepComponent::deep_sleep_() { void DeepSleepComponent::deep_sleep_() {
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) // Timer wakeup - all variants support this
if (this->sleep_duration_.has_value()) if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_); esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
// Single pin wakeup (ext0) - ESP32, S2, S3 only
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
if (this->wakeup_pin_ != nullptr) { if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin()); const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) { if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
@@ -95,32 +119,15 @@ void DeepSleepComponent::deep_sleep_() {
} }
esp_sleep_enable_ext0_wakeup(gpio_pin, level); esp_sleep_enable_ext0_wakeup(gpio_pin, level);
} }
if (this->ext1_wakeup_.has_value()) {
esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
}
if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) {
esp_sleep_enable_touchpad_wakeup();
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
}
#endif #endif
#if defined(USE_ESP32_VARIANT_ESP32H2) // GPIO wakeup - C2, C3, C6 only
if (this->sleep_duration_.has_value()) #if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->ext1_wakeup_.has_value()) {
esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
}
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) { if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin()); const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLUP) { if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY); gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
} else if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLDOWN) { } else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY); gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
} }
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
@@ -138,9 +145,26 @@ void DeepSleepComponent::deep_sleep_() {
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level)); static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
} }
#endif #endif
// Multiple pin wakeup (ext1) - All except C2, C3
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
if (this->ext1_wakeup_.has_value()) {
esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
}
#endif
// Touch wakeup - ESP32, S2, S3 only
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) {
esp_sleep_enable_touchpad_wakeup();
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
}
#endif
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
} // namespace deep_sleep } // namespace deep_sleep
} // namespace esphome } // namespace esphome
#endif #endif // USE_ESP32

View File

@@ -296,14 +296,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
def _format_framework_espidf_version( def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
ver: cv.Version, release: str, for_platformio: bool # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
) -> str:
# format the given arduino (https://github.com/espressif/esp-idf/releases) version to
# a PIO platformio/framework-espidf value # a PIO platformio/framework-espidf value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
if for_platformio:
return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
if release: if release:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
@@ -317,157 +312,108 @@ def _format_framework_espidf_version(
# The default/recommended arduino framework version # The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
# The platform-espressif32 version to use for arduino frameworks "recommended": cv.Version(3, 2, 1),
# - https://github.com/pioarduino/platform-espressif32/releases "latest": cv.Version(3, 3, 1),
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") "dev": cv.Version(3, 3, 1),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 1): cv.Version(55, 3, 31),
cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
cv.Version(3, 2, 0): cv.Version(54, 3, 20),
cv.Version(3, 1, 3): cv.Version(53, 3, 13),
cv.Version(3, 1, 2): cv.Version(53, 3, 12),
cv.Version(3, 1, 1): cv.Version(53, 3, 11),
cv.Version(3, 1, 0): cv.Version(53, 3, 10),
}
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) "recommended": cv.Version(5, 4, 2),
# The platformio/espressif32 version to use for esp-idf frameworks "latest": cv.Version(5, 5, 1),
# - https://github.com/platformio/platform-espressif32/releases "dev": cv.Version(5, 5, 1),
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 }
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 1): cv.Version(55, 3, 31),
cv.Version(5, 5, 0): cv.Version(55, 3, 31),
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
cv.Version(5, 3, 2): cv.Version(53, 3, 13),
cv.Version(5, 3, 1): cv.Version(53, 3, 13),
cv.Version(5, 3, 0): cv.Version(53, 3, 13),
cv.Version(5, 1, 6): cv.Version(51, 3, 7),
cv.Version(5, 1, 5): cv.Version(51, 3, 7),
}
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions # The platform-espressif32 version
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ # - https://github.com/pioarduino/platform-espressif32/releases
cv.Version(5, 3, 1), PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 3, 0), "recommended": cv.Version(54, 3, 21, "2"),
cv.Version(5, 2, 2), "latest": cv.Version(55, 3, 31),
cv.Version(5, 2, 1), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
cv.Version(5, 1, 2), }
cv.Version(5, 1, 1),
cv.Version(5, 1, 0),
cv.Version(5, 0, 2),
cv.Version(5, 0, 1),
cv.Version(5, 0, 0),
]
# 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),
cv.Version(5, 4, 0),
cv.Version(5, 3, 3),
cv.Version(5, 3, 2),
cv.Version(5, 3, 1),
cv.Version(5, 3, 0),
cv.Version(5, 1, 5),
cv.Version(5, 1, 6),
]
def _check_versions(value): def _check_versions(value):
value = value.copy() 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),
}
if value[CONF_VERSION] in lookups: if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
if CONF_SOURCE in value: if CONF_SOURCE in value or CONF_PLATFORM_VERSION 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."
)
return value
lookups = {
"dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 2, 2), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
}
if value[CONF_VERSION] in lookups:
if CONF_SOURCE in value:
raise cv.Invalid( raise cv.Invalid(
"Framework version needs to be explicitly specified when custom source is used." "Version needs to be explicitly set when a custom source or platform_version is used."
) )
version, source = lookups[value[CONF_VERSION]] platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
else:
version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
else: else:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
if version < cv.Version(5, 0, 0):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
# flag this for later *before* we set value[CONF_PLATFORM_VERSION] below
has_platform_ver = CONF_PLATFORM_VERSION in value
value[CONF_PLATFORM_VERSION] = value.get(
CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION))
)
if (
is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION])
) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X:
raise cv.Invalid(
f"ESP-IDF {str(version)} not supported by platformio/espressif32"
)
if (
version in SUPPORTED_PLATFORMIO_ESP_IDF_5X
and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
) and not has_platform_ver:
raise cv.Invalid(
f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'"
)
if (
not is_platformio
and CONF_RELEASE not in value
and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
):
raise cv.Invalid(
f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'"
)
value[CONF_VERSION] = str(version) value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_espidf_version(
version, value.get(CONF_RELEASE, None), is_platformio
)
if value[CONF_SOURCE].startswith("http"): if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
# prefix is necessary or platformio will complain with a cryptic error if version < cv.Version(3, 0, 0):
value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}" raise cv.Invalid("Only Arduino 3.0+ is supported.")
recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version)
value[CONF_SOURCE] = value.get(
CONF_SOURCE, _format_framework_arduino_version(version)
)
else:
if version < cv.Version(5, 0, 0):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
value[CONF_SOURCE] = value.get(
CONF_SOURCE,
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
)
if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: if CONF_PLATFORM_VERSION not in value:
if platform_lookup is None:
raise cv.Invalid(
"Framework version not recognized; please specify platform_version"
)
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
if version != recommended_version:
_LOGGER.warning( _LOGGER.warning(
"The selected ESP-IDF framework version is not the recommended one. " "The selected framework version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version."
)
if value[CONF_PLATFORM_VERSION] != _parse_platform_version(
str(PLATFORM_VERSION_LOOKUP["recommended"])
):
_LOGGER.warning(
"The selected platform version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version." "If there are connectivity or build issues please remove the manual version."
) )
@@ -477,26 +423,14 @@ def _check_versions(value):
def _parse_platform_version(value): def _parse_platform_version(value):
try: try:
ver = cv.Version.parse(cv.version_number(value)) ver = cv.Version.parse(cv.version_number(value))
if ver.major >= 50: # a pioarduino version release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" if ver.extra:
if ver.extra: release += f"-{ver.extra}"
release += f"-{ver.extra}" return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
# if platform version is a valid version constraint, prefix the default package
cv.platformio_version_constraint(value)
return f"platformio/espressif32@{value}"
except cv.Invalid: except cv.Invalid:
return value return value
def _platform_is_platformio(value):
try:
ver = cv.Version.parse(cv.version_number(value))
return ver.major < 50
except cv.Invalid:
return "platformio" in value
def _detect_variant(value): def _detect_variant(value):
board = value.get(CONF_BOARD) board = value.get(CONF_BOARD)
variant = value.get(CONF_VARIANT) variant = value.get(CONF_VARIANT)
@@ -808,6 +742,8 @@ async def to_code(config):
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
@@ -850,8 +786,6 @@ async def to_code(config):
cg.add_build_flag("-Wno-nonnull-compare") 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_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option( add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True

View File

@@ -1,5 +1,8 @@
from collections.abc import Callable, MutableMapping
from enum import Enum from enum import Enum
import logging
import re import re
from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
@@ -9,16 +12,19 @@ from esphome.const import (
CONF_ENABLE_ON_BOOT, CONF_ENABLE_ON_BOOT,
CONF_ESPHOME, CONF_ESPHOME,
CONF_ID, CONF_ID,
CONF_MAX_CONNECTIONS,
CONF_NAME, CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX, CONF_NAME_ADD_MAC_SUFFIX,
) )
from esphome.core import TimePeriod from esphome.core import CORE, TimePeriod
import esphome.final_validate as fv import esphome.final_validate as fv
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble" DOMAIN = "esp32_ble"
_LOGGER = logging.getLogger(__name__)
class BTLoggers(Enum): class BTLoggers(Enum):
"""Bluetooth logger categories available in ESP-IDF. """Bluetooth logger categories available in ESP-IDF.
@@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs"
CONF_CONNECTION_TIMEOUT = "connection_timeout" CONF_CONNECTION_TIMEOUT = "connection_timeout"
CONF_MAX_NOTIFICATIONS = "max_notifications" CONF_MAX_NOTIFICATIONS = "max_notifications"
# BLE connection limits
# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4
# Total instances: 10 (ADV + SCAN + connections)
# - ADV only: up to 9 connections
# - SCAN only: up to 9 connections
# - ADV + SCAN: up to 8 connections
DEFAULT_MAX_CONNECTIONS = 3
IDF_MAX_CONNECTIONS = 9
# Connection slot tracking keys
KEY_ESP32_BLE = "esp32_ble"
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
# Export for use by other components (bluetooth_proxy, etc.)
__all__ = [
"DEFAULT_MAX_CONNECTIONS",
"IDF_MAX_CONNECTIONS",
"KEY_ESP32_BLE",
"KEY_USED_CONNECTION_SLOTS",
"consume_connection_slots",
]
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
@@ -183,6 +211,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.positive_int, cv.positive_int,
cv.Range(min=1, max=64), cv.Range(min=1, max=64),
), ),
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@@ -230,6 +261,56 @@ def validate_variant(_):
raise cv.Invalid(f"{variant} does not support Bluetooth") raise cv.Invalid(f"{variant} does not support Bluetooth")
def consume_connection_slots(
value: int, consumer: str
) -> Callable[[MutableMapping], MutableMapping]:
"""Reserve BLE connection slots for a component.
Args:
value: Number of connection slots to reserve
consumer: Name of the component consuming the slots
Returns:
A validator function that records the slot usage
"""
def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {})
slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
slots.extend([consumer] * value)
return config
return _consume_connection_slots
def validate_connection_slots(max_connections: int) -> None:
"""Validate that BLE connection slots don't exceed the configured maximum."""
ble_data = CORE.data.get(KEY_ESP32_BLE, {})
used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
num_used = len(used_slots)
if num_used <= max_connections:
return
slot_users = ", ".join(used_slots)
if num_used > IDF_MAX_CONNECTIONS:
raise cv.Invalid(
f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. "
f"Reduce the number of BLE clients. Components: {slot_users}"
)
_LOGGER.warning(
"BLE components require %d connection slot(s) but only %d configured. "
"Please set 'max_connections: %d' in the 'esp32_ble' component. "
"Components: %s",
num_used,
max_connections,
num_used,
slot_users,
)
def final_validation(config): def final_validation(config):
validate_variant(config) validate_variant(config)
if (name := config.get(CONF_NAME)) is not None: if (name := config.get(CONF_NAME)) is not None:
@@ -245,6 +326,10 @@ def final_validation(config):
# Set GATT Client/Server sdkconfig options based on which components are loaded # Set GATT Client/Server sdkconfig options based on which components are loaded
full_config = fv.full_config.get() full_config = fv.full_config.get()
# Validate connection slots usage
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
validate_connection_slots(max_connections)
# Check if BLE Server is needed # Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config has_ble_server = "esp32_ble_server" in full_config
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server) add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
@@ -255,6 +340,26 @@ def final_validation(config):
) )
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
# Handle max_connections: check for deprecated location in esp32_ble_tracker
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
# Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat)
if "esp32_ble_tracker" in full_config:
tracker_config = full_config["esp32_ble_tracker"]
if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config:
max_connections = tracker_config["max_connections"]
# Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN
# This is the Bluedroid host stack total instance limit (range 1-9, default 4)
# Total instances = ADV/SCAN (1) + connection slots (max_connections)
# Shared between client (tracker/ble_client) and server
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1)
# Set controller-specific max connections for ESP32 (classic)
# CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN)
# For newer chips (C3/S3/etc), different configs are used automatically
add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections)
return config return config
@@ -270,6 +375,10 @@ async def to_code(config):
cg.add(var.set_name(name)) cg.add(var.set_name(name))
await cg.register_component(var, config) await cg.register_component(var, config)
# Define max connections for use in C++ code (e.g., ble_server.h)
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)

View File

@@ -213,15 +213,17 @@ bool ESP32BLE::ble_setup_() {
if (this->name_.has_value()) { if (this->name_.has_value()) {
name = this->name_.value(); name = this->name_.value();
if (App.is_name_add_mac_suffix_enabled()) { if (App.is_name_add_mac_suffix_enabled()) {
name += "-" + get_mac_address().substr(6); name += "-";
name += get_mac_address().substr(6);
} }
} else { } else {
name = App.get_name(); name = App.get_name();
if (name.length() > 20) { if (name.length() > 20) {
if (App.is_name_add_mac_suffix_enabled()) { if (App.is_name_add_mac_suffix_enabled()) {
name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address // Keep first 13 chars and last 7 chars (MAC suffix), remove middle
name.erase(13, name.length() - 20);
} else { } else {
name = name.substr(0, 20); name.resize(20);
} }
} }
} }

View File

@@ -26,7 +26,7 @@ from esphome.const import (
from esphome.core import CORE from esphome.core import CORE
from esphome.schema_extractors import SCHEMA_EXTRACT from esphome.schema_extractors import SCHEMA_EXTRACT
AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"] AUTO_LOAD = ["esp32_ble", "bytebuffer"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
DOMAIN = "esp32_ble_server" DOMAIN = "esp32_ble_server"

View File

@@ -49,7 +49,11 @@ void BLECharacteristic::notify() {
this->service_->get_server()->get_connected_client_count() == 0) this->service_->get_server()->get_connected_client_count() == 0)
return; return;
for (auto &client : this->service_->get_server()->get_clients()) { const uint16_t *clients = this->service_->get_server()->get_clients();
uint8_t client_count = this->service_->get_server()->get_client_count();
for (uint8_t i = 0; i < client_count; i++) {
uint16_t client = clients[i];
size_t length = this->value_.size(); size_t length = this->value_.size();
// Find the client in the list of clients to notify // Find the client in the list of clients to notify
auto *entry = this->find_client_in_notify_list_(client); auto *entry = this->find_client_in_notify_list_(client);
@@ -73,7 +77,7 @@ void BLECharacteristic::notify() {
void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) {
// If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified
if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) { if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &value, uint16_t conn_id) { descriptor->on_write([this](std::span<const uint8_t> value, uint16_t conn_id) {
if (value.size() != 2) if (value.size() != 2)
return; return;
uint16_t cccd = encode_uint16(value[1], value[0]); uint16_t cccd = encode_uint16(value[1], value[0]);
@@ -208,8 +212,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
if (!param->read.need_rsp) if (!param->read.need_rsp)
break; // For some reason you can request a read but not want a response break; // For some reason you can request a read but not want a response
this->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ, if (this->on_read_callback_) {
param->read.conn_id); (*this->on_read_callback_)(param->read.conn_id);
}
uint16_t max_offset = 22; uint16_t max_offset = 22;
@@ -277,8 +282,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
} }
if (!param->write.is_prep) { if (!param->write.is_prep) {
this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_( if (this->on_write_callback_) {
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id); (*this->on_write_callback_)(this->value_, param->write.conn_id);
}
} }
break; break;
@@ -289,8 +295,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
break; break;
this->write_event_ = false; this->write_event_ = false;
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_( if (this->on_write_callback_) {
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id); (*this->on_write_callback_)(this->value_, param->exec_write.conn_id);
}
} }
esp_err_t err = esp_err_t err =
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr);

View File

@@ -2,10 +2,12 @@
#include "ble_descriptor.h" #include "ble_descriptor.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/components/bytebuffer/bytebuffer.h"
#include <vector> #include <vector>
#include <span>
#include <functional>
#include <memory>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -22,22 +24,10 @@ namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
using namespace event_emitter;
class BLEService; class BLEService;
namespace BLECharacteristicEvt { class BLECharacteristic {
enum VectorEvt {
ON_WRITE,
};
enum EmptyEvt {
ON_READ,
};
} // namespace BLECharacteristicEvt
class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>,
public EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t> {
public: public:
BLECharacteristic(ESPBTUUID uuid, uint32_t properties); BLECharacteristic(ESPBTUUID uuid, uint32_t properties);
~BLECharacteristic(); ~BLECharacteristic();
@@ -76,6 +66,15 @@ class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, s
bool is_created(); bool is_created();
bool is_failed(); bool is_failed();
// Direct callback registration - only allocates when callback is set
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
this->on_write_callback_ =
std::make_unique<std::function<void(std::span<const uint8_t>, uint16_t)>>(std::move(callback));
}
void on_read(std::function<void(uint16_t)> &&callback) {
this->on_read_callback_ = std::make_unique<std::function<void(uint16_t)>>(std::move(callback));
}
protected: protected:
bool write_event_{false}; bool write_event_{false};
BLEService *service_{}; BLEService *service_{};
@@ -98,6 +97,9 @@ class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, s
void remove_client_from_notify_list_(uint16_t conn_id); void remove_client_from_notify_list_(uint16_t conn_id);
ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id); ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id);
std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_;
esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;
enum State : uint8_t { enum State : uint8_t {

View File

@@ -74,9 +74,10 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break; break;
this->value_.attr_len = param->write.len; this->value_.attr_len = param->write.len;
memcpy(this->value_.attr_value, param->write.value, param->write.len); memcpy(this->value_.attr_value, param->write.value, param->write.len);
this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE, if (this->on_write_callback_) {
std::vector<uint8_t>(param->write.value, param->write.value + param->write.len), (*this->on_write_callback_)(std::span<const uint8_t>(param->write.value, param->write.len),
param->write.conn_id); param->write.conn_id);
}
break; break;
} }
default: default:

View File

@@ -1,30 +1,26 @@
#pragma once #pragma once
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_gatt_defs.h> #include <esp_gatt_defs.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
#include <span>
#include <functional>
#include <memory>
namespace esphome { namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
using namespace event_emitter;
class BLECharacteristic; class BLECharacteristic;
namespace BLEDescriptorEvt { // Base class for BLE descriptors
enum VectorEvt { class BLEDescriptor {
ON_WRITE,
};
} // namespace BLEDescriptorEvt
class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t> {
public: public:
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true); BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
virtual ~BLEDescriptor(); virtual ~BLEDescriptor();
@@ -39,6 +35,12 @@ class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vect
bool is_created() { return this->state_ == CREATED; } bool is_created() { return this->state_ == CREATED; }
bool is_failed() { return this->state_ == FAILED; } bool is_failed() { return this->state_ == FAILED; }
// Direct callback registration - only allocates when callback is set
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
this->on_write_callback_ =
std::make_unique<std::function<void(std::span<const uint8_t>, uint16_t)>>(std::move(callback));
}
protected: protected:
BLECharacteristic *characteristic_{nullptr}; BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_; ESPBTUUID uuid_;
@@ -46,6 +48,8 @@ class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vect
esp_attr_value_t value_{}; esp_attr_value_t value_{};
std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
esp_gatt_perm_t permissions_{}; esp_gatt_perm_t permissions_{};
enum State : uint8_t { enum State : uint8_t {

View File

@@ -147,20 +147,28 @@ BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
return nullptr; return nullptr;
} }
void BLEServer::dispatch_callbacks_(CallbackType type, uint16_t conn_id) {
for (auto &entry : this->callbacks_) {
if (entry.type == type) {
entry.callback(conn_id);
}
}
}
void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GATTS_CONNECT_EVT: { case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected"); ESP_LOGD(TAG, "BLE Client connected");
this->add_client_(param->connect.conn_id); this->add_client_(param->connect.conn_id);
this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id); this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
break; break;
} }
case ESP_GATTS_DISCONNECT_EVT: { case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected"); ESP_LOGD(TAG, "BLE Client disconnected");
this->remove_client_(param->disconnect.conn_id); this->remove_client_(param->disconnect.conn_id);
this->parent_->advertising_start(); this->parent_->advertising_start();
this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id); this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
break; break;
} }
case ESP_GATTS_REG_EVT: { case ESP_GATTS_REG_EVT: {
@@ -177,9 +185,38 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
} }
} }
int8_t BLEServer::find_client_index_(uint16_t conn_id) const {
for (uint8_t i = 0; i < this->client_count_; i++) {
if (this->clients_[i] == conn_id)
return i;
}
return -1;
}
void BLEServer::add_client_(uint16_t conn_id) {
// Check if already in list
if (this->find_client_index_(conn_id) >= 0)
return;
// Add if there's space
if (this->client_count_ < USE_ESP32_BLE_MAX_CONNECTIONS) {
this->clients_[this->client_count_++] = conn_id;
} else {
// This should never happen since max clients is known at compile time
ESP_LOGE(TAG, "Client array full");
}
}
void BLEServer::remove_client_(uint16_t conn_id) {
int8_t index = this->find_client_index_(conn_id);
if (index >= 0) {
// Replace with last element and decrement count (client order not preserved)
this->clients_[index] = this->clients_[--this->client_count_];
}
}
void BLEServer::ble_before_disabled_event_handler() { void BLEServer::ble_before_disabled_event_handler() {
// Delete all clients // Delete all clients
this->clients_.clear(); this->client_count_ = 0;
// Delete all services // Delete all services
for (auto &entry : this->services_) { for (auto &entry : this->services_) {
entry.service->do_delete(); entry.service->do_delete();

View File

@@ -12,7 +12,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <functional>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -24,18 +24,7 @@ namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
namespace BLEServerEvt { class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> {
enum EmptyEvt {
ON_CONNECT,
ON_DISCONNECT,
};
} // namespace BLEServerEvt
class BLEServer : public Component,
public GATTsEventHandler,
public BLEStatusEventHandler,
public Parented<ESP32BLE>,
public EventEmitter<BLEServerEvt::EmptyEvt, uint16_t> {
public: public:
void setup() override; void setup() override;
void loop() override; void loop() override;
@@ -57,15 +46,34 @@ class BLEServer : public Component,
void set_device_information_service(BLEService *service) { this->device_information_service_ = service; } void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
uint32_t get_connected_client_count() { return this->clients_.size(); } uint32_t get_connected_client_count() { return this->client_count_; }
const std::unordered_set<uint16_t> &get_clients() { return this->clients_; } const uint16_t *get_clients() const { return this->clients_; }
uint8_t get_client_count() const { return this->client_count_; }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) override; esp_ble_gatts_cb_param_t *param) override;
void ble_before_disabled_event_handler() override; void ble_before_disabled_event_handler() override;
// Direct callback registration - supports multiple callbacks
void on_connect(std::function<void(uint16_t)> &&callback) {
this->callbacks_.push_back({CallbackType::ON_CONNECT, std::move(callback)});
}
void on_disconnect(std::function<void(uint16_t)> &&callback) {
this->callbacks_.push_back({CallbackType::ON_DISCONNECT, std::move(callback)});
}
protected: protected:
enum class CallbackType : uint8_t {
ON_CONNECT,
ON_DISCONNECT,
};
struct CallbackEntry {
CallbackType type;
std::function<void(uint16_t)> callback;
};
struct ServiceEntry { struct ServiceEntry {
ESPBTUUID uuid; ESPBTUUID uuid;
uint8_t inst_id; uint8_t inst_id;
@@ -74,14 +82,19 @@ class BLEServer : public Component,
void restart_advertising_(); void restart_advertising_();
void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } int8_t find_client_index_(uint16_t conn_id) const;
void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } void add_client_(uint16_t conn_id);
void remove_client_(uint16_t conn_id);
void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
std::vector<CallbackEntry> callbacks_;
std::vector<uint8_t> manufacturer_data_{}; std::vector<uint8_t> manufacturer_data_{};
esp_gatt_if_t gatts_if_{0}; esp_gatt_if_t gatts_if_{0};
bool registered_{false}; bool registered_{false};
std::unordered_set<uint16_t> clients_; uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
uint8_t client_count_{0};
std::vector<ServiceEntry> services_{}; std::vector<ServiceEntry> services_{};
std::vector<BLEService *> services_to_start_{}; std::vector<BLEService *> services_to_start_{};
BLEService *device_information_service_{}; BLEService *device_information_service_{};

View File

@@ -14,9 +14,10 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
BLECharacteristic *characteristic) { BLECharacteristic *characteristic) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on( characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
BLECharacteristicEvt::VectorEvt::ON_WRITE, // Convert span to vector for trigger
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); }); on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger; return on_write_trigger;
} }
#endif #endif
@@ -25,9 +26,10 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on( descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
BLEDescriptorEvt::VectorEvt::ON_WRITE, // Convert span to vector for trigger
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); }); on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger; return on_write_trigger;
} }
#endif #endif
@@ -35,8 +37,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
#ifdef USE_ESP32_BLE_SERVER_ON_CONNECT #ifdef USE_ESP32_BLE_SERVER_ON_CONNECT
Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory) Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on(BLEServerEvt::EmptyEvt::ON_CONNECT, server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
[on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
return on_connect_trigger; return on_connect_trigger;
} }
#endif #endif
@@ -44,38 +45,22 @@ Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *serv
#ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT #ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT
Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory) Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
[on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
return on_disconnect_trigger; return on_disconnect_trigger;
} }
#endif #endif
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic, void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic,
EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener) { const std::function<void()> &pre_notify_listener) {
// Find and remove existing listener for this characteristic // Find and remove existing listener for this characteristic
auto *existing = this->find_listener_(characteristic); auto *existing = this->find_listener_(characteristic);
if (existing != nullptr) { if (existing != nullptr) {
// Remove the previous listener
characteristic->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::off(BLECharacteristicEvt::EmptyEvt::ON_READ,
existing->listener_id);
// Remove the pre-notify listener
this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, existing->pre_notify_listener_id);
// Remove from vector // Remove from vector
this->remove_listener_(characteristic); this->remove_listener_(characteristic);
} }
// Create a new listener for the pre-notify event
EventEmitterListenerID pre_notify_listener_id =
this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY,
[pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) {
// Only call the pre-notify listener if the characteristic is the one we are interested in
if (characteristic == evt_characteristic) {
pre_notify_listener();
}
});
// Save the entry to the vector // Save the entry to the vector
this->listeners_.push_back({characteristic, listener_id, pre_notify_listener_id}); this->listeners_.push_back({characteristic, pre_notify_listener});
} }
BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_( BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_(

View File

@@ -4,7 +4,6 @@
#include "ble_characteristic.h" #include "ble_characteristic.h"
#include "ble_descriptor.h" #include "ble_descriptor.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include <vector> #include <vector>
@@ -18,10 +17,6 @@ namespace esp32_ble_server {
namespace esp32_ble_server_automations { namespace esp32_ble_server_automations {
using namespace esp32_ble; using namespace esp32_ble;
using namespace event_emitter;
// Invalid listener ID constant - 0 is used as sentinel value in EventEmitter
static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0;
class BLETriggers { class BLETriggers {
public: public:
@@ -41,38 +36,29 @@ class BLETriggers {
}; };
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
enum BLECharacteristicSetValueActionEvt {
PRE_NOTIFY,
};
// Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic // Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic
class BLECharacteristicSetValueActionManager class BLECharacteristicSetValueActionManager {
: public EventEmitter<BLECharacteristicSetValueActionEvt, BLECharacteristic *> {
public: public:
// Singleton pattern // Singleton pattern
static BLECharacteristicSetValueActionManager *get_instance() { static BLECharacteristicSetValueActionManager *get_instance() {
static BLECharacteristicSetValueActionManager instance; static BLECharacteristicSetValueActionManager instance;
return &instance; return &instance;
} }
void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id, void set_listener(BLECharacteristic *characteristic, const std::function<void()> &pre_notify_listener);
const std::function<void()> &pre_notify_listener); bool has_listener(BLECharacteristic *characteristic) { return this->find_listener_(characteristic) != nullptr; }
EventEmitterListenerID get_listener(BLECharacteristic *characteristic) { void emit_pre_notify(BLECharacteristic *characteristic) {
for (const auto &entry : this->listeners_) { for (const auto &entry : this->listeners_) {
if (entry.characteristic == characteristic) { if (entry.characteristic == characteristic) {
return entry.listener_id; entry.pre_notify_listener();
break;
} }
} }
return INVALID_LISTENER_ID;
}
void emit_pre_notify(BLECharacteristic *characteristic) {
this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic);
} }
private: private:
struct ListenerEntry { struct ListenerEntry {
BLECharacteristic *characteristic; BLECharacteristic *characteristic;
EventEmitterListenerID listener_id; std::function<void()> pre_notify_listener;
EventEmitterListenerID pre_notify_listener_id;
}; };
std::vector<ListenerEntry> listeners_; std::vector<ListenerEntry> listeners_;
@@ -87,24 +73,22 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(Ts... x) override { void play(Ts... x) override {
// If the listener is already set, do nothing // If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_) if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_))
return; return;
// Set initial value // Set initial value
this->parent_->set_value(this->buffer_.value(x...)); this->parent_->set_value(this->buffer_.value(x...));
// Set the listener for read events // Set the listener for read events
this->listener_id_ = this->parent_->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::on( this->parent_->on_read([this, x...](uint16_t id) {
BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) { // Set the value of the characteristic every time it is read
// Set the value of the characteristic every time it is read this->parent_->set_value(this->buffer_.value(x...));
this->parent_->set_value(this->buffer_.value(x...)); });
});
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
BLECharacteristicSetValueActionManager::get_instance()->set_listener( BLECharacteristicSetValueActionManager::get_instance()->set_listener(
this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); this->parent_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); });
} }
protected: protected:
BLECharacteristic *parent_; BLECharacteristic *parent_;
EventEmitterListenerID listener_id_;
}; };
#endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION

View File

@@ -1,14 +1,13 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, MutableMapping
import logging import logging
from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import ( from esphome.components.esp32_ble import (
IDF_MAX_CONNECTIONS,
BTLoggers, BTLoggers,
bt_uuid, bt_uuid,
bt_uuid16_format, bt_uuid16_format,
@@ -24,6 +23,7 @@ from esphome.const import (
CONF_INTERVAL, CONF_INTERVAL,
CONF_MAC_ADDRESS, CONF_MAC_ADDRESS,
CONF_MANUFACTURER_ID, CONF_MANUFACTURER_ID,
CONF_MAX_CONNECTIONS,
CONF_ON_BLE_ADVERTISE, CONF_ON_BLE_ADVERTISE,
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
@@ -38,19 +38,12 @@ AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@bdraco"] CODEOWNERS = ["@bdraco"]
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
CONF_MAX_CONNECTIONS = "max_connections"
CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_ESP32_BLE_ID = "esp32_ble_id"
CONF_SCAN_PARAMETERS = "scan_parameters" CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_WINDOW = "window" CONF_WINDOW = "window"
CONF_ON_SCAN_END = "on_scan_end" CONF_ON_SCAN_END = "on_scan_end"
CONF_SOFTWARE_COEXISTENCE = "software_coexistence" CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
DEFAULT_MAX_CONNECTIONS = 3
IDF_MAX_CONNECTIONS = 9
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -128,6 +121,15 @@ def validate_scan_parameters(config):
return config return config
def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
if CONF_MAX_CONNECTIONS in config:
_LOGGER.warning(
"The 'max_connections' option in 'esp32_ble_tracker' is deprecated. "
"Please move it to the 'esp32_ble' component instead."
)
return config
def as_hex(value): def as_hex(value):
return cg.RawExpression(f"0x{value}ULL") return cg.RawExpression(f"0x{value}ULL")
@@ -150,24 +152,12 @@ def as_reversed_hex_array(value):
) )
def consume_connection_slots(
value: int, consumer: str
) -> Callable[[MutableMapping], MutableMapping]:
def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {})
slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
slots.extend([consumer] * value)
return config
return _consume_connection_slots
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESP32BLETracker), cv.GenerateID(): cv.declare_id(ESP32BLETracker),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), 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.Optional(CONF_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS) cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS)
), ),
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
@@ -224,48 +214,11 @@ CONFIG_SCHEMA = cv.All(
cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool, cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool,
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
validate_max_connections_deprecated,
) )
def validate_remaining_connections(config): FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {})
slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, [])
used_slots = len(slots)
if used_slots <= config[CONF_MAX_CONNECTIONS]:
return config
slot_users = ", ".join(slots)
if used_slots < IDF_MAX_CONNECTIONS:
_LOGGER.warning(
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
"connection slot(s) out of available configured maximum %d connection "
"slot(s); The system automatically increased `%s` to %d to match the "
"number of used connection slot(s) by components: %s.",
CONF_MAX_CONNECTIONS,
used_slots,
config[CONF_MAX_CONNECTIONS],
CONF_MAX_CONNECTIONS,
used_slots,
slot_users,
)
config[CONF_MAX_CONNECTIONS] = used_slots
return config
msg = (
f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: "
f"components attempted to consume {used_slots} connection slot(s) "
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:
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
raise cv.Invalid(msg)
FINAL_VALIDATE_SCHEMA = cv.All(
validate_remaining_connections, esp32_ble.validate_variant
)
ESP_BLE_DEVICE_SCHEMA = cv.Schema( ESP_BLE_DEVICE_SCHEMA = cv.Schema(
{ {
@@ -345,10 +298,8 @@ async def to_code(config):
# Match arduino CONFIG_BTU_TASK_STACK_SIZE # Match arduino CONFIG_BTU_TASK_STACK_SIZE
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 # 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_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
add_idf_sdkconfig_option( # configured in esp32_ble component based on max_connections setting
"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_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT") cg.add_define("USE_ESP32_BLE_CLIENT")

View File

@@ -67,8 +67,16 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
} }
bool ESP32Can::setup_internal() { bool ESP32Can::setup_internal() {
static int next_twai_ctrl_num = 0;
if (next_twai_ctrl_num >= SOC_TWAI_CONTROLLER_NUM) {
ESP_LOGW(TAG, "Maximum number of esp32_can components created already");
this->mark_failed();
return false;
}
twai_general_config_t g_config = twai_general_config_t g_config =
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL);
g_config.controller_id = next_twai_ctrl_num++;
if (this->tx_queue_len_.has_value()) { if (this->tx_queue_len_.has_value()) {
g_config.tx_queue_len = this->tx_queue_len_.value(); g_config.tx_queue_len = this->tx_queue_len_.value();
} }
@@ -86,14 +94,14 @@ bool ESP32Can::setup_internal() {
} }
// Install TWAI driver // Install TWAI driver
if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { if (twai_driver_install_v2(&g_config, &t_config, &f_config, &(this->twai_handle_)) != ESP_OK) {
// Failed to install driver // Failed to install driver
this->mark_failed(); this->mark_failed();
return false; return false;
} }
// Start TWAI driver // Start TWAI driver
if (twai_start() != ESP_OK) { if (twai_start_v2(this->twai_handle_) != ESP_OK) {
// Failed to start driver // Failed to start driver
this->mark_failed(); this->mark_failed();
return false; return false;
@@ -102,6 +110,11 @@ bool ESP32Can::setup_internal() {
} }
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
if (this->twai_handle_ == nullptr) {
// not setup yet or setup failed
return canbus::ERROR_FAIL;
}
if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) {
return canbus::ERROR_FAILTX; return canbus::ERROR_FAILTX;
} }
@@ -124,7 +137,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
memcpy(message.data, frame->data, frame->can_data_length_code); memcpy(message.data, frame->data, frame->can_data_length_code);
} }
if (twai_transmit(&message, this->tx_enqueue_timeout_ticks_) == ESP_OK) { if (twai_transmit_v2(this->twai_handle_, &message, this->tx_enqueue_timeout_ticks_) == ESP_OK) {
return canbus::ERROR_OK; return canbus::ERROR_OK;
} else { } else {
return canbus::ERROR_ALLTXBUSY; return canbus::ERROR_ALLTXBUSY;
@@ -132,9 +145,14 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
} }
canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) {
if (this->twai_handle_ == nullptr) {
// not setup yet or setup failed
return canbus::ERROR_FAIL;
}
twai_message_t message; twai_message_t message;
if (twai_receive(&message, 0) != ESP_OK) { if (twai_receive_v2(this->twai_handle_, &message, 0) != ESP_OK) {
return canbus::ERROR_NOMSG; return canbus::ERROR_NOMSG;
} }

View File

@@ -5,6 +5,8 @@
#include "esphome/components/canbus/canbus.h" #include "esphome/components/canbus/canbus.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <driver/twai.h>
namespace esphome { namespace esphome {
namespace esp32_can { namespace esp32_can {
@@ -29,6 +31,7 @@ class ESP32Can : public canbus::Canbus {
TickType_t tx_enqueue_timeout_ticks_{}; TickType_t tx_enqueue_timeout_ticks_{};
optional<uint32_t> tx_queue_len_{}; optional<uint32_t> tx_queue_len_{};
optional<uint32_t> rx_queue_len_{}; optional<uint32_t> rx_queue_len_{};
twai_handle_t twai_handle_{nullptr};
}; };
} // namespace esp32_can } // namespace esp32_can

View File

@@ -38,8 +38,7 @@ void ESP32ImprovComponent::setup() {
}); });
} }
#endif #endif
global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
[this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
// Start with loop disabled - will be enabled by start() when needed // Start with loop disabled - will be enabled by start() when needed
this->disable_loop(); this->disable_loop();
@@ -57,12 +56,11 @@ void ESP32ImprovComponent::setup_characteristics() {
this->error_->add_descriptor(error_descriptor); this->error_->add_descriptor(error_descriptor);
this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on( this->rpc_->on_write([this](std::span<const uint8_t> data, uint16_t id) {
BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &data, uint16_t id) { if (!data.empty()) {
if (!data.empty()) { this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); }
} });
});
BLEDescriptor *rpc_descriptor = new BLE2902(); BLEDescriptor *rpc_descriptor = new BLE2902();
this->rpc_->add_descriptor(rpc_descriptor); this->rpc_->add_descriptor(rpc_descriptor);

View File

@@ -35,7 +35,7 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
if (symbols_free < RMT_SYMBOLS_PER_BYTE) { if (symbols_free < RMT_SYMBOLS_PER_BYTE) {
return 0; return 0;
} }
for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { for (size_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) {
if (bytes[index] & (1 << (7 - i))) { if (bytes[index] & (1 << (7 - i))) {
symbols[i] = params->bit1; symbols[i] = params->bit1;
} else { } else {

View File

@@ -1,11 +1,13 @@
#include "ota_esphome.h" #include "ota_esphome.h"
#ifdef USE_OTA #ifdef USE_OTA
#ifdef USE_OTA_PASSWORD
#ifdef USE_OTA_MD5 #ifdef USE_OTA_MD5
#include "esphome/components/md5/md5.h" #include "esphome/components/md5/md5.h"
#endif #endif
#ifdef USE_OTA_SHA256 #ifdef USE_OTA_SHA256
#include "esphome/components/sha256/sha256.h" #include "esphome/components/sha256/sha256.h"
#endif #endif
#endif
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_arduino_esp32.h" #include "esphome/components/ota/ota_backend_arduino_esp32.h"
@@ -26,9 +28,19 @@ namespace esphome {
static const char *const TAG = "esphome.ota"; static const char *const TAG = "esphome.ota";
static constexpr uint16_t OTA_BLOCK_SIZE = 8192; static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
#ifdef USE_OTA_PASSWORD
#ifdef USE_OTA_MD5
static constexpr size_t MD5_HEX_SIZE = 32; // MD5 hash as hex string (16 bytes * 2)
#endif
#ifdef USE_OTA_SHA256
static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2)
#endif
#endif // USE_OTA_PASSWORD
void ESPHomeOTAComponent::setup() { void ESPHomeOTAComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK #ifdef USE_OTA_STATE_CALLBACK
ota::register_ota_platform(this); ota::register_ota_platform(this);
@@ -69,7 +81,7 @@ void ESPHomeOTAComponent::setup() {
return; return;
} }
err = this->server_->listen(4); err = this->server_->listen(1); // Only one client at a time
if (err != 0) { if (err != 0) {
this->log_socket_error_(LOG_STR("listen")); this->log_socket_error_(LOG_STR("listen"));
this->mark_failed(); this->mark_failed();
@@ -112,11 +124,11 @@ static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
#define ALLOW_OTA_DOWNGRADE_MD5 #define ALLOW_OTA_DOWNGRADE_MD5
void ESPHomeOTAComponent::handle_handshake_() { void ESPHomeOTAComponent::handle_handshake_() {
/// Handle the initial OTA handshake. /// Handle the OTA handshake and authentication.
/// ///
/// This method is non-blocking and will return immediately if no data is available. /// This method is non-blocking and will return immediately if no data is available.
/// It reads all 5 magic bytes (0x6C, 0x26, 0xF7, 0x5C, 0x45) non-blocking /// It manages the state machine through connection, magic bytes validation, feature
/// before proceeding to handle_data_(). A 10-second timeout is enforced from initial connection. /// negotiation, and authentication before entering the blocking data transfer phase.
if (this->client_ == nullptr) { if (this->client_ == nullptr) {
// We already checked server_->ready() in loop(), so we can accept directly // We already checked server_->ready() in loop(), so we can accept directly
@@ -141,7 +153,8 @@ void ESPHomeOTAComponent::handle_handshake_() {
} }
this->log_start_(LOG_STR("handshake")); this->log_start_(LOG_STR("handshake"));
this->client_connect_time_ = App.get_loop_component_start_time(); this->client_connect_time_ = App.get_loop_component_start_time();
this->magic_buf_pos_ = 0; // Reset magic buffer position this->handshake_buf_pos_ = 0; // Reset handshake buffer position
this->ota_state_ = OTAState::MAGIC_READ;
} }
// Check for handshake timeout // Check for handshake timeout
@@ -152,46 +165,99 @@ void ESPHomeOTAComponent::handle_handshake_() {
return; return;
} }
// Try to read remaining magic bytes switch (this->ota_state_) {
if (this->magic_buf_pos_ < 5) { case OTAState::MAGIC_READ: {
// Read as many bytes as available // Try to read remaining magic bytes (5 total)
uint8_t bytes_to_read = 5 - this->magic_buf_pos_; if (!this->try_read_(5, LOG_STR("read magic"))) {
ssize_t read = this->client_->read(this->magic_buf_ + this->magic_buf_pos_, bytes_to_read); return;
if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
return; // No data yet, try again next loop
}
if (read <= 0) {
// Error or connection closed
if (read == -1) {
this->log_socket_error_(LOG_STR("reading magic bytes"));
} else {
ESP_LOGW(TAG, "Remote closed during handshake");
} }
this->cleanup_connection_();
return; // Validate magic bytes
static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
return;
}
// Magic bytes valid, move to next state
this->transition_ota_state_(OTAState::MAGIC_ACK);
this->handshake_buf_[0] = ota::OTA_RESPONSE_OK;
this->handshake_buf_[1] = USE_OTA_VERSION;
[[fallthrough]];
} }
this->magic_buf_pos_ += read; case OTAState::MAGIC_ACK: {
} // Send OK and version - 2 bytes
if (!this->try_write_(2, LOG_STR("ack magic"))) {
// Check if we have all 5 magic bytes return;
if (this->magic_buf_pos_ == 5) { }
// Validate magic bytes // All bytes sent, create backend and move to next state
static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; this->backend_ = ota::make_ota_backend();
if (memcmp(this->magic_buf_, MAGIC_BYTES, 5) != 0) { this->transition_ota_state_(OTAState::FEATURE_READ);
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->magic_buf_[0], [[fallthrough]];
this->magic_buf_[1], this->magic_buf_[2], this->magic_buf_[3], this->magic_buf_[4]);
// Send error response (non-blocking, best effort)
uint8_t error = static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_MAGIC);
this->client_->write(&error, 1);
this->cleanup_connection_();
return;
} }
// All 5 magic bytes are valid, continue with data handling case OTAState::FEATURE_READ: {
this->handle_data_(); // Read features - 1 byte
if (!this->try_read_(1, LOG_STR("read feature"))) {
return;
}
this->ota_features_ = this->handshake_buf_[0];
ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
this->transition_ota_state_(OTAState::FEATURE_ACK);
this->handshake_buf_[0] =
((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression())
? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION
: ota::OTA_RESPONSE_HEADER_OK;
[[fallthrough]];
}
case OTAState::FEATURE_ACK: {
// Acknowledge header - 1 byte
if (!this->try_write_(1, LOG_STR("ack feature"))) {
return;
}
#ifdef USE_OTA_PASSWORD
// If password is set, move to auth phase
if (!this->password_.empty()) {
this->transition_ota_state_(OTAState::AUTH_SEND);
} else
#endif
{
// No password, move directly to data phase
this->transition_ota_state_(OTAState::DATA);
}
[[fallthrough]];
}
#ifdef USE_OTA_PASSWORD
case OTAState::AUTH_SEND: {
// Non-blocking authentication send
if (!this->handle_auth_send_()) {
return;
}
this->transition_ota_state_(OTAState::AUTH_READ);
[[fallthrough]];
}
case OTAState::AUTH_READ: {
// Non-blocking authentication read & verify
if (!this->handle_auth_read_()) {
return;
}
this->transition_ota_state_(OTAState::DATA);
[[fallthrough]];
}
#endif
case OTAState::DATA:
this->handle_data_();
return;
default:
break;
} }
} }
@@ -199,114 +265,21 @@ void ESPHomeOTAComponent::handle_data_() {
/// Handle the OTA data transfer and update process. /// Handle the OTA data transfer and update process.
/// ///
/// This method is blocking and will not return until the OTA update completes, /// This method is blocking and will not return until the OTA update completes,
/// fails, or times out. It handles authentication, receives the firmware data, /// fails, or times out. It receives the firmware data, writes it to flash,
/// writes it to flash, and reboots on success. /// and reboots on success.
///
/// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ.
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
bool update_started = false; bool update_started = false;
size_t total = 0; size_t total = 0;
uint32_t last_progress = 0; uint32_t last_progress = 0;
uint8_t buf[1024]; uint8_t buf[OTA_BUFFER_SIZE];
char *sbuf = reinterpret_cast<char *>(buf); char *sbuf = reinterpret_cast<char *>(buf);
size_t ota_size; size_t ota_size;
uint8_t ota_features;
std::unique_ptr<ota::OTABackend> backend;
(void) ota_features;
#if USE_OTA_VERSION == 2 #if USE_OTA_VERSION == 2
size_t size_acknowledged = 0; size_t size_acknowledged = 0;
#endif #endif
// Send OK and version - 2 bytes
buf[0] = ota::OTA_RESPONSE_OK;
buf[1] = USE_OTA_VERSION;
this->writeall_(buf, 2);
backend = ota::make_ota_backend();
// Read features - 1 byte
if (!this->readall_(buf, 1)) {
this->log_read_error_(LOG_STR("features"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
ota_features = buf[0]; // NOLINT
ESP_LOGV(TAG, "Features: 0x%02X", ota_features);
// Acknowledge header - 1 byte
buf[0] = ota::OTA_RESPONSE_HEADER_OK;
if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) {
buf[0] = ota::OTA_RESPONSE_SUPPORTS_COMPRESSION;
}
this->writeall_(buf, 1);
#ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) {
bool auth_success = false;
#ifdef USE_OTA_SHA256
// SECURITY HARDENING: Prefer SHA256 authentication on platforms that support it.
//
// This is a hardening measure to prevent future downgrade attacks where an attacker
// could force the use of MD5 authentication by manipulating the feature flags.
//
// While MD5 is currently still acceptable for our OTA authentication use case
// (where the password is a shared secret and we're only authenticating, not
// encrypting), at some point in the future MD5 will likely become so weak that
// it could be practically attacked.
//
// We enforce SHA256 now on capable platforms because:
// 1. We can't retroactively update device firmware in the field
// 2. Clients (like esphome CLI) can always be updated to support SHA256
// 3. This prevents any possibility of downgrade attacks in the future
//
// Devices that don't support SHA256 (due to platform limitations) will
// continue to use MD5 as their only option (see #else branch below).
bool client_supports_sha256 = (ota_features & FEATURE_SUPPORTS_SHA256_AUTH) != 0;
#ifdef ALLOW_OTA_DOWNGRADE_MD5
// Temporary compatibility mode: Allow MD5 for ~3 versions to enable OTA downgrades
// This prevents users from being locked out if they need to downgrade after updating
// TODO: Remove this entire ifdef block in 2026.1.0
if (client_supports_sha256) {
sha256::SHA256 sha_hasher;
auth_success = this->perform_hash_auth_(&sha_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_SHA256_AUTH,
LOG_STR("SHA256"), sbuf);
} else {
#ifdef USE_OTA_MD5
ESP_LOGW(TAG, "Using MD5 auth for compatibility (deprecated)");
md5::MD5Digest md5_hasher;
auth_success =
this->perform_hash_auth_(&md5_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_AUTH, LOG_STR("MD5"), sbuf);
#endif // USE_OTA_MD5
}
#else
// Strict mode: SHA256 required on capable platforms (future default)
if (!client_supports_sha256) {
ESP_LOGW(TAG, "Client requires SHA256");
error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
sha256::SHA256 sha_hasher;
auth_success = this->perform_hash_auth_(&sha_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_SHA256_AUTH,
LOG_STR("SHA256"), sbuf);
#endif // ALLOW_OTA_DOWNGRADE_MD5
#else
// Platform only supports MD5 - use it as the only available option
// This is not a security downgrade as the platform cannot support SHA256
#ifdef USE_OTA_MD5
md5::MD5Digest md5_hasher;
auth_success =
this->perform_hash_auth_(&md5_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_AUTH, LOG_STR("MD5"), sbuf);
#endif // USE_OTA_MD5
#endif // USE_OTA_SHA256
if (!auth_success) {
error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
}
#endif // USE_OTA_PASSWORD
// Acknowledge auth OK - 1 byte // Acknowledge auth OK - 1 byte
buf[0] = ota::OTA_RESPONSE_AUTH_OK; buf[0] = ota::OTA_RESPONSE_AUTH_OK;
this->writeall_(buf, 1); this->writeall_(buf, 1);
@@ -334,7 +307,7 @@ void ESPHomeOTAComponent::handle_data_() {
#endif #endif
// This will block for a few seconds as it locks flash // This will block for a few seconds as it locks flash
error_code = backend->begin(ota_size); error_code = this->backend_->begin(ota_size);
if (error_code != ota::OTA_RESPONSE_OK) if (error_code != ota::OTA_RESPONSE_OK)
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
update_started = true; update_started = true;
@@ -350,7 +323,7 @@ void ESPHomeOTAComponent::handle_data_() {
} }
sbuf[32] = '\0'; sbuf[32] = '\0';
ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf); ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf);
backend->set_update_md5(sbuf); this->backend_->set_update_md5(sbuf);
// Acknowledge MD5 OK - 1 byte // Acknowledge MD5 OK - 1 byte
buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
@@ -358,26 +331,24 @@ void ESPHomeOTAComponent::handle_data_() {
while (total < ota_size) { while (total < ota_size) {
// TODO: timeout check // TODO: timeout check
size_t requested = std::min(sizeof(buf), ota_size - total); size_t remaining = ota_size - total;
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read = this->client_->read(buf, requested); ssize_t read = this->client_->read(buf, requested);
if (read == -1) { if (read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) { if (this->would_block_(errno)) {
this->yield_and_feed_watchdog_(); this->yield_and_feed_watchdog_();
continue; continue;
} }
ESP_LOGW(TAG, "Read error, errno %d", errno); ESP_LOGW(TAG, "Read err %d", errno);
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} else if (read == 0) { } else if (read == 0) {
// $ man recv ESP_LOGW(TAG, "Remote closed");
// "When a stream socket peer has performed an orderly shutdown, the return value will
// be 0 (the traditional "end-of-file" return)."
ESP_LOGW(TAG, "Remote closed connection");
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
error_code = backend->write(buf, read); error_code = this->backend_->write(buf, read);
if (error_code != ota::OTA_RESPONSE_OK) { if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Flash write error, code: %d", error_code); ESP_LOGW(TAG, "Flash write err %d", error_code);
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
total += read; total += read;
@@ -406,9 +377,9 @@ void ESPHomeOTAComponent::handle_data_() {
buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
this->writeall_(buf, 1); this->writeall_(buf, 1);
error_code = backend->end(); error_code = this->backend_->end();
if (error_code != ota::OTA_RESPONSE_OK) { if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Error ending update! code: %d", error_code); ESP_LOGW(TAG, "End update err %d", error_code);
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
@@ -437,8 +408,8 @@ error:
this->writeall_(buf, 1); this->writeall_(buf, 1);
this->cleanup_connection_(); this->cleanup_connection_();
if (backend != nullptr && update_started) { if (this->backend_ != nullptr && update_started) {
backend->abort(); this->backend_->abort();
} }
this->status_momentary_error("onerror", 5000); this->status_momentary_error("onerror", 5000);
@@ -459,12 +430,12 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
ssize_t read = this->client_->read(buf + at, len - at); ssize_t read = this->client_->read(buf + at, len - at);
if (read == -1) { if (read == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) { if (!this->would_block_(errno)) {
ESP_LOGW(TAG, "Error reading %d bytes, errno %d", len, errno); ESP_LOGW(TAG, "Read err %d bytes, errno %d", len, errno);
return false; return false;
} }
} else if (read == 0) { } else if (read == 0) {
ESP_LOGW(TAG, "Remote closed connection"); ESP_LOGW(TAG, "Remote closed");
return false; return false;
} else { } else {
at += read; at += read;
@@ -486,8 +457,8 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
ssize_t written = this->client_->write(buf + at, len - at); ssize_t written = this->client_->write(buf + at, len - at);
if (written == -1) { if (written == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) { if (!this->would_block_(errno)) {
ESP_LOGW(TAG, "Error writing %d bytes, errno %d", len, errno); ESP_LOGW(TAG, "Write err %d bytes, errno %d", len, errno);
return false; return false;
} }
} else { } else {
@@ -512,11 +483,74 @@ void ESPHomeOTAComponent::log_start_(const LogString *phase) {
ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str()); ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str());
} }
void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) {
ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during));
}
bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) {
if (read == -1 && this->would_block_(errno)) {
return false; // No data yet, try again next loop
}
if (read <= 0) {
read == 0 ? this->log_remote_closed_(desc) : this->log_socket_error_(desc);
this->cleanup_connection_();
return false;
}
return true;
}
bool ESPHomeOTAComponent::handle_write_error_(ssize_t written, const LogString *desc) {
if (written == -1) {
if (this->would_block_(errno)) {
return false; // Try again next loop
}
this->log_socket_error_(desc);
this->cleanup_connection_();
return false;
}
return true;
}
bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *desc) {
// Read bytes into handshake buffer, starting at handshake_buf_pos_
size_t bytes_to_read = to_read - this->handshake_buf_pos_;
ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read);
if (!this->handle_read_error_(read, desc)) {
return false;
}
this->handshake_buf_pos_ += read;
// Return true only if we have all the requested bytes
return this->handshake_buf_pos_ >= to_read;
}
bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *desc) {
// Write bytes from handshake buffer, starting at handshake_buf_pos_
size_t bytes_to_write = to_write - this->handshake_buf_pos_;
ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write);
if (!this->handle_write_error_(written, desc)) {
return false;
}
this->handshake_buf_pos_ += written;
// Return true only if we have written all the requested bytes
return this->handshake_buf_pos_ >= to_write;
}
void ESPHomeOTAComponent::cleanup_connection_() { void ESPHomeOTAComponent::cleanup_connection_() {
this->client_->close(); this->client_->close();
this->client_ = nullptr; this->client_ = nullptr;
this->client_connect_time_ = 0; this->client_connect_time_ = 0;
this->magic_buf_pos_ = 0; this->handshake_buf_pos_ = 0;
this->ota_state_ = OTAState::IDLE;
this->ota_features_ = 0;
this->backend_ = nullptr;
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
#endif
} }
void ESPHomeOTAComponent::yield_and_feed_watchdog_() { void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
@@ -525,82 +559,253 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
} }
#ifdef USE_OTA_PASSWORD #ifdef USE_OTA_PASSWORD
void ESPHomeOTAComponent::log_auth_warning_(const LogString *action, const LogString *hash_name) { void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); }
ESP_LOGW(TAG, "Auth: %s %s failed", LOG_STR_ARG(action), LOG_STR_ARG(hash_name));
bool ESPHomeOTAComponent::select_auth_type_() {
#ifdef USE_OTA_SHA256
bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0;
#ifdef ALLOW_OTA_DOWNGRADE_MD5
// Allow fallback to MD5 if client doesn't support SHA256
if (client_supports_sha256) {
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH;
return true;
}
#ifdef USE_OTA_MD5
this->log_auth_warning_(LOG_STR("Using deprecated MD5"));
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH;
return true;
#else
this->log_auth_warning_(LOG_STR("SHA256 required"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
#endif // USE_OTA_MD5
#else // !ALLOW_OTA_DOWNGRADE_MD5
// Require SHA256
if (!client_supports_sha256) {
this->log_auth_warning_(LOG_STR("SHA256 required"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
}
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH;
return true;
#endif // ALLOW_OTA_DOWNGRADE_MD5
#else // !USE_OTA_SHA256
#ifdef USE_OTA_MD5
// Only MD5 available
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH;
return true;
#else
// No auth methods available
this->log_auth_warning_(LOG_STR("No auth methods available"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
#endif // USE_OTA_MD5
#endif // USE_OTA_SHA256
} }
// Non-template function definition to reduce binary size bool ESPHomeOTAComponent::handle_auth_send_() {
bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string &password, uint8_t auth_request, // Initialize auth buffer if not already done
const LogString *name, char *buf) { if (!this->auth_buf_) {
// Get sizes from the hasher // Select auth type based on client capabilities and configuration
const size_t hex_size = hasher->get_size() * 2; // Hex is twice the byte size if (!this->select_auth_type_()) {
const size_t nonce_len = hasher->get_size() / 4; // Nonce is 1/4 of hash size in bytes return false;
}
// Use the provided buffer for all hex operations // Generate nonce - hasher must be created and used in same stack frame
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
// 1. Hash objects must NEVER be passed to another function (different stack frame)
// 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA
// 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created
// Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption.
//
// Buffer layout after AUTH_READ completes:
// [0]: auth_type (1 byte)
// [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
// Small stack buffer for nonce seed bytes // Declare both hash objects in same stack frame, use pointer to select.
uint8_t nonce_bytes[8]; // Max 8 bytes (2 x uint32_t for SHA256) // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
// hardware SHA acceleration - the object must exist in this stack frame for all operations.
// Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
#ifdef USE_OTA_SHA256
sha256::SHA256 sha_hasher;
#endif
#ifdef USE_OTA_MD5
md5::MD5Digest md5_hasher;
#endif
HashBase *hasher = nullptr;
// Send auth request type #ifdef USE_OTA_SHA256
this->writeall_(&auth_request, 1); if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
hasher = &sha_hasher;
}
#endif
#ifdef USE_OTA_MD5
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
hasher = &md5_hasher;
}
#endif
const size_t hex_size = hasher->get_size() * 2;
const size_t nonce_len = hasher->get_size() / 4;
const size_t auth_buf_size = 1 + 3 * hex_size;
this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
this->auth_buf_pos_ = 0;
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
this->log_auth_warning_(LOG_STR("Random failed"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
return false;
}
hasher->init();
hasher->add(buf, nonce_len);
hasher->calculate();
this->auth_buf_[0] = this->auth_type_;
hasher->get_hex(buf);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
memcpy(log_buf, buf, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
#endif
}
// Try to write auth_type + nonce
size_t hex_size = this->get_auth_hex_size_();
const size_t to_write = 1 + hex_size;
size_t remaining = to_write - this->auth_buf_pos_;
ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining);
if (!this->handle_write_error_(written, LOG_STR("ack auth"))) {
return false;
}
this->auth_buf_pos_ += written;
// Check if we still have more to write
if (this->auth_buf_pos_ < to_write) {
return false; // More to write, try again next loop
}
// All written, prepare for reading phase
this->auth_buf_pos_ = 0;
return true;
}
bool ESPHomeOTAComponent::handle_auth_read_() {
size_t hex_size = this->get_auth_hex_size_();
const size_t to_read = hex_size * 2; // CNonce + Response
// Try to read remaining bytes (CNonce + Response)
// We read cnonce+response starting at offset 1+hex_size (after auth_type and our nonce)
size_t cnonce_offset = 1 + hex_size; // Offset where cnonce should be stored in buffer
size_t remaining = to_read - this->auth_buf_pos_;
ssize_t read = this->client_->read(this->auth_buf_.get() + cnonce_offset + this->auth_buf_pos_, remaining);
if (!this->handle_read_error_(read, LOG_STR("read auth"))) {
return false;
}
this->auth_buf_pos_ += read;
// Check if we still need more data
if (this->auth_buf_pos_ < to_read) {
return false; // More to read, try again next loop
}
// We have all the data, verify it
const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
const char *cnonce = nonce + hex_size;
const char *response = cnonce + hex_size;
// CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions).
// Declare both hash objects in same stack frame, use pointer to select.
// NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
// hardware SHA acceleration - the object must exist in this stack frame for all operations.
// Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
#ifdef USE_OTA_SHA256
sha256::SHA256 sha_hasher;
#endif
#ifdef USE_OTA_MD5
md5::MD5Digest md5_hasher;
#endif
HashBase *hasher = nullptr;
#ifdef USE_OTA_SHA256
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
hasher = &sha_hasher;
}
#endif
#ifdef USE_OTA_MD5
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
hasher = &md5_hasher;
}
#endif
hasher->init(); hasher->init();
hasher->add(this->password_.c_str(), this->password_.length());
// Generate nonce seed bytes using random_bytes hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
if (!random_bytes(nonce_bytes, nonce_len)) {
this->log_auth_warning_(LOG_STR("Random bytes generation failed"), name);
return false;
}
hasher->add(nonce_bytes, nonce_len);
hasher->calculate(); hasher->calculate();
// Generate and send nonce #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
hasher->get_hex(buf); char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
buf[hex_size] = '\0'; // Log CNonce
ESP_LOGV(TAG, "Auth: %s Nonce is %s", LOG_STR_ARG(name), buf); memcpy(log_buf, cnonce, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf);
if (!this->writeall_(reinterpret_cast<uint8_t *>(buf), hex_size)) { // Log computed hash
this->log_auth_warning_(LOG_STR("Writing nonce"), name); hasher->get_hex(log_buf);
return false; log_buf[hex_size] = '\0';
} ESP_LOGV(TAG, "Auth: Result is %s", log_buf);
// Start challenge: password + nonce // Log received response
hasher->init(); memcpy(log_buf, response, hex_size);
hasher->add(password.c_str(), password.length()); log_buf[hex_size] = '\0';
hasher->add(buf, hex_size); ESP_LOGV(TAG, "Auth: Response is %s", log_buf);
#endif
// Read cnonce and add to hash // Compare response
if (!this->readall_(reinterpret_cast<uint8_t *>(buf), hex_size)) { bool matches = hasher->equals_hex(response);
this->log_auth_warning_(LOG_STR("Reading cnonce"), name);
return false;
}
buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s CNonce is %s", LOG_STR_ARG(name), buf);
hasher->add(buf, hex_size);
hasher->calculate();
// Log expected result (digest is already in hasher)
hasher->get_hex(buf);
buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s Result is %s", LOG_STR_ARG(name), buf);
// Read response into the buffer
if (!this->readall_(reinterpret_cast<uint8_t *>(buf), hex_size)) {
this->log_auth_warning_(LOG_STR("Reading response"), name);
return false;
}
buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s Response is %s", LOG_STR_ARG(name), buf);
// Compare response directly with digest in hasher
bool matches = hasher->equals_hex(buf);
if (!matches) { if (!matches) {
this->log_auth_warning_(LOG_STR("Password mismatch"), name); this->log_auth_warning_(LOG_STR("Password mismatch"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
} }
return matches; // Authentication successful - clean up auth state
this->cleanup_auth_();
return true;
}
size_t ESPHomeOTAComponent::get_auth_hex_size_() const {
#ifdef USE_OTA_SHA256
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
return SHA256_HEX_SIZE;
}
#endif
#ifdef USE_OTA_MD5
return MD5_HEX_SIZE;
#else
#ifndef USE_OTA_SHA256
#error "Either USE_OTA_MD5 or USE_OTA_SHA256 must be defined when USE_OTA_PASSWORD is enabled"
#endif
#endif
}
void ESPHomeOTAComponent::cleanup_auth_() {
this->auth_buf_ = nullptr;
this->auth_buf_pos_ = 0;
this->auth_type_ = 0;
} }
#endif // USE_OTA_PASSWORD #endif // USE_OTA_PASSWORD

View File

@@ -14,6 +14,18 @@ namespace esphome {
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
class ESPHomeOTAComponent : public ota::OTAComponent { class ESPHomeOTAComponent : public ota::OTAComponent {
public: public:
enum class OTAState : uint8_t {
IDLE,
MAGIC_READ, // Reading magic bytes
MAGIC_ACK, // Sending OK and version after magic bytes
FEATURE_READ, // Reading feature flags from client
FEATURE_ACK, // Sending feature acknowledgment
#ifdef USE_OTA_PASSWORD
AUTH_SEND, // Sending authentication request
AUTH_READ, // Reading authentication data
#endif // USE_OTA_PASSWORD
DATA, // BLOCKING! Processing OTA data (update, etc.)
};
#ifdef USE_OTA_PASSWORD #ifdef USE_OTA_PASSWORD
void set_auth_password(const std::string &password) { password_ = password; } void set_auth_password(const std::string &password) { password_ = password; }
#endif // USE_OTA_PASSWORD #endif // USE_OTA_PASSWORD
@@ -32,16 +44,37 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
void handle_handshake_(); void handle_handshake_();
void handle_data_(); void handle_data_();
#ifdef USE_OTA_PASSWORD #ifdef USE_OTA_PASSWORD
bool perform_hash_auth_(HashBase *hasher, const std::string &password, uint8_t auth_request, const LogString *name, bool handle_auth_send_();
char *buf); bool handle_auth_read_();
void log_auth_warning_(const LogString *action, const LogString *hash_name); bool select_auth_type_();
size_t get_auth_hex_size_() const;
void cleanup_auth_();
void log_auth_warning_(const LogString *msg);
#endif // USE_OTA_PASSWORD #endif // USE_OTA_PASSWORD
bool readall_(uint8_t *buf, size_t len); bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len);
bool try_read_(size_t to_read, const LogString *desc);
bool try_write_(size_t to_write, const LogString *desc);
inline bool would_block_(int error_code) const { return error_code == EAGAIN || error_code == EWOULDBLOCK; }
bool handle_read_error_(ssize_t read, const LogString *desc);
bool handle_write_error_(ssize_t written, const LogString *desc);
inline void transition_ota_state_(OTAState next_state) {
this->ota_state_ = next_state;
this->handshake_buf_pos_ = 0; // Reset buffer position for next state
}
void log_socket_error_(const LogString *msg); void log_socket_error_(const LogString *msg);
void log_read_error_(const LogString *what); void log_read_error_(const LogString *what);
void log_start_(const LogString *phase); void log_start_(const LogString *phase);
void log_remote_closed_(const LogString *during);
void cleanup_connection_(); void cleanup_connection_();
inline void send_error_and_cleanup_(ota::OTAResponseTypes error) {
uint8_t error_byte = static_cast<uint8_t>(error);
this->client_->write(&error_byte, 1); // Best effort, non-blocking
this->cleanup_connection_();
}
void yield_and_feed_watchdog_(); void yield_and_feed_watchdog_();
#ifdef USE_OTA_PASSWORD #ifdef USE_OTA_PASSWORD
@@ -50,11 +83,19 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
std::unique_ptr<socket::Socket> server_; std::unique_ptr<socket::Socket> server_;
std::unique_ptr<socket::Socket> client_; std::unique_ptr<socket::Socket> client_;
std::unique_ptr<ota::OTABackend> backend_;
uint32_t client_connect_time_{0}; uint32_t client_connect_time_{0};
uint16_t port_; uint16_t port_;
uint8_t magic_buf_[5]; uint8_t handshake_buf_[5];
uint8_t magic_buf_pos_{0}; OTAState ota_state_{OTAState::IDLE};
uint8_t handshake_buf_pos_{0};
uint8_t ota_features_{0};
#ifdef USE_OTA_PASSWORD
std::unique_ptr<uint8_t[]> auth_buf_;
uint8_t auth_buf_pos_{0};
uint8_t auth_type_{0}; // Store auth type to know which hasher to use
#endif // USE_OTA_PASSWORD
}; };
} // namespace esphome } // namespace esphome

View File

@@ -27,6 +27,7 @@ from esphome.const import (
CONF_GATEWAY, CONF_GATEWAY,
CONF_ID, CONF_ID,
CONF_INTERRUPT_PIN, CONF_INTERRUPT_PIN,
CONF_MAC_ADDRESS,
CONF_MANUAL_IP, CONF_MANUAL_IP,
CONF_MISO_PIN, CONF_MISO_PIN,
CONF_MODE, CONF_MODE,
@@ -197,6 +198,7 @@ BASE_SCHEMA = cv.Schema(
"This option has been removed. Please use the [disabled] option under the " "This option has been removed. Please use the [disabled] option under the "
"new mdns component instead." "new mdns component instead."
), ),
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@@ -365,6 +367,9 @@ async def to_code(config):
if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]): if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
cg.add_define(phy_define) cg.add_define(phy_define)
if mac_address := config.get(CONF_MAC_ADDRESS):
cg.add(var.set_fixed_mac(mac_address.parts))
cg.add_define("USE_ETHERNET") cg.add_define("USE_ETHERNET")
# Disable WiFi when using Ethernet to save memory # Disable WiFi when using Ethernet to save memory

View File

@@ -41,17 +41,20 @@ static const char *const TAG = "ethernet";
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
this->mark_failed();
}
#define ESPHL_ERROR_CHECK(err, message) \ #define ESPHL_ERROR_CHECK(err, message) \
if ((err) != ESP_OK) { \ if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ this->log_error_and_mark_failed_(err, message); \
this->mark_failed(); \
return; \ return; \
} }
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ #define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \ if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ this->log_error_and_mark_failed_(err, message); \
this->mark_failed(); \
return ret; \ return ret; \
} }
@@ -253,7 +256,11 @@ void EthernetComponent::setup() {
// use ESP internal eth mac // use ESP internal eth mac
uint8_t mac_addr[6]; uint8_t mac_addr[6];
esp_read_mac(mac_addr, ESP_MAC_ETH); if (this->fixed_mac_.has_value()) {
memcpy(mac_addr, this->fixed_mac_->data(), 6);
} else {
esp_read_mac(mac_addr, ESP_MAC_ETH);
}
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr); err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr);
ESPHL_ERROR_CHECK(err, "set mac address error"); ESPHL_ERROR_CHECK(err, "set mac address error");

View File

@@ -84,6 +84,7 @@ class EthernetComponent : public Component {
#endif #endif
void set_type(EthernetType type); void set_type(EthernetType type);
void set_manual_ip(const ManualIP &manual_ip); void set_manual_ip(const ManualIP &manual_ip);
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
network::IPAddresses get_ip_addresses(); network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num); network::IPAddress get_dns_address(uint8_t num);
@@ -105,6 +106,7 @@ class EthernetComponent : public Component {
void start_connect_(); void start_connect_();
void finish_connect_(); void finish_connect_();
void dump_connect_params_(); void dump_connect_params_();
void log_error_and_mark_failed_(esp_err_t err, const char *message);
#ifdef USE_ETHERNET_KSZ8081 #ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081. /// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
@@ -155,12 +157,13 @@ class EthernetComponent : public Component {
esp_netif_t *eth_netif_{nullptr}; esp_netif_t *eth_netif_{nullptr};
esp_eth_handle_t eth_handle_; esp_eth_handle_t eth_handle_;
esp_eth_phy_t *phy_{nullptr}; esp_eth_phy_t *phy_{nullptr};
optional<std::array<uint8_t, 6>> fixed_mac_;
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern EthernetComponent *global_eth_component; extern EthernetComponent *global_eth_component;
#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
#endif #endif

View File

@@ -1,5 +0,0 @@
CODEOWNERS = ["@Rapsssito"]
# Allows event_emitter to be configured in yaml, to allow use of the C++ api.
CONFIG_SCHEMA = {}

View File

@@ -1,117 +0,0 @@
#pragma once
#include <vector>
#include <functional>
#include <limits>
#include "esphome/core/log.h"
namespace esphome {
namespace event_emitter {
using EventEmitterListenerID = uint32_t;
static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0;
// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this)
// and a list of arguments. Supports multiple listeners for each event.
template<typename EvtType, typename... Args> class EventEmitter {
public:
EventEmitterListenerID on(EvtType event, std::function<void(Args...)> listener) {
EventEmitterListenerID listener_id = this->get_next_id_();
// Find or create event entry
EventEntry *entry = this->find_or_create_event_(event);
entry->listeners.push_back({listener_id, listener});
return listener_id;
}
void off(EvtType event, EventEmitterListenerID id) {
EventEntry *entry = this->find_event_(event);
if (entry == nullptr)
return;
// Remove listener with given id
for (auto it = entry->listeners.begin(); it != entry->listeners.end(); ++it) {
if (it->id == id) {
// Swap with last and pop for efficient removal
*it = entry->listeners.back();
entry->listeners.pop_back();
// Remove event entry if no more listeners
if (entry->listeners.empty()) {
this->remove_event_(event);
}
return;
}
}
}
protected:
void emit_(EvtType event, Args... args) {
EventEntry *entry = this->find_event_(event);
if (entry == nullptr)
return;
// Call all listeners for this event
for (const auto &listener : entry->listeners) {
listener.callback(args...);
}
}
private:
struct Listener {
EventEmitterListenerID id;
std::function<void(Args...)> callback;
};
struct EventEntry {
EvtType event;
std::vector<Listener> listeners;
};
EventEmitterListenerID get_next_id_() {
// Simple incrementing ID, wrapping around at max
EventEmitterListenerID next_id = (this->current_id_ + 1);
if (next_id == INVALID_LISTENER_ID) {
next_id = 1;
}
this->current_id_ = next_id;
return this->current_id_;
}
EventEntry *find_event_(EvtType event) {
for (auto &entry : this->events_) {
if (entry.event == event) {
return &entry;
}
}
return nullptr;
}
EventEntry *find_or_create_event_(EvtType event) {
EventEntry *entry = this->find_event_(event);
if (entry != nullptr)
return entry;
// Create new event entry
this->events_.push_back({event, {}});
return &this->events_.back();
}
void remove_event_(EvtType event) {
for (auto it = this->events_.begin(); it != this->events_.end(); ++it) {
if (it->event == event) {
// Swap with last and pop
*it = this->events_.back();
this->events_.pop_back();
return;
}
}
}
std::vector<EventEntry> events_;
EventEmitterListenerID current_id_ = 0;
};
} // namespace event_emitter
} // namespace esphome

View File

@@ -7,24 +7,20 @@ namespace hdc1080 {
static const char *const TAG = "hdc1080"; static const char *const TAG = "hdc1080";
static const uint8_t HDC1080_ADDRESS = 0x40; // 0b1000000 from datasheet
static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02; static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02;
static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00; static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00;
static const uint8_t HDC1080_CMD_HUMIDITY = 0x01; static const uint8_t HDC1080_CMD_HUMIDITY = 0x01;
void HDC1080Component::setup() { void HDC1080Component::setup() {
const uint8_t data[2] = { const uint8_t config[2] = {0x00, 0x00}; // resolution 14bit for both humidity and temperature
0b00000000, // resolution 14bit for both humidity and temperature
0b00000000 // reserved
};
if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { // if configuration fails - there is a problem
// as instruction is same as powerup defaults (for now), interpret as warning if this fails if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "HDC1080 initial config instruction error"); this->mark_failed();
this->status_set_warning();
return; return;
} }
} }
void HDC1080Component::dump_config() { void HDC1080Component::dump_config() {
ESP_LOGCONFIG(TAG, "HDC1080:"); ESP_LOGCONFIG(TAG, "HDC1080:");
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
@@ -35,39 +31,51 @@ void HDC1080Component::dump_config() {
LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Humidity", this->humidity_);
} }
void HDC1080Component::update() { void HDC1080Component::update() {
uint16_t raw_temp; // regardless of what sensor/s are defined in yaml configuration
// the hdc1080 setup configuration used, requires both temperature and humidity to be read
this->status_clear_warning();
if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) { if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
delay(20);
if (this->read(reinterpret_cast<uint8_t *>(&raw_temp), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
raw_temp = i2c::i2ctohs(raw_temp);
float temp = raw_temp * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40
this->temperature_->publish_state(temp);
uint16_t raw_humidity; this->set_timeout(20, [this]() {
if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) { uint16_t raw_temperature;
this->status_set_warning(); if (this->read(reinterpret_cast<uint8_t *>(&raw_temperature), 2) != i2c::ERROR_OK) {
return; this->status_set_warning();
} return;
delay(20); }
if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
raw_humidity = i2c::i2ctohs(raw_humidity);
float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100
this->humidity_->publish_state(humidity);
ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temp, humidity); if (this->temperature_ != nullptr) {
this->status_clear_warning(); raw_temperature = i2c::i2ctohs(raw_temperature);
float temperature = raw_temperature * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40
this->temperature_->publish_state(temperature);
}
if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
this->set_timeout(20, [this]() {
uint16_t raw_humidity;
if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
if (this->humidity_ != nullptr) {
raw_humidity = i2c::i2ctohs(raw_humidity);
float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100
this->humidity_->publish_state(humidity);
}
});
});
} }
float HDC1080Component::get_setup_priority() const { return setup_priority::DATA; }
} // namespace hdc1080 } // namespace hdc1080
} // namespace esphome } // namespace esphome

View File

@@ -12,13 +12,11 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice {
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
/// Setup the sensor and check for connection.
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
/// Retrieve the latest sensor values. This operation takes approximately 16ms.
void update() override; void update() override;
float get_setup_priority() const override; float get_setup_priority() const override { return setup_priority::DATA; }
protected: protected:
sensor::Sensor *temperature_{nullptr}; sensor::Sensor *temperature_{nullptr};

View File

@@ -9,6 +9,7 @@ from esphome.const import (
CONF_ID, CONF_ID,
CONF_METHOD, CONF_METHOD,
CONF_ON_ERROR, CONF_ON_ERROR,
CONF_ON_RESPONSE,
CONF_TIMEOUT, CONF_TIMEOUT,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_URL, CONF_URL,
@@ -52,7 +53,6 @@ CONF_BUFFER_SIZE_TX = "buffer_size_tx"
CONF_CA_CERTIFICATE_PATH = "ca_certificate_path" CONF_CA_CERTIFICATE_PATH = "ca_certificate_path"
CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size"
CONF_ON_RESPONSE = "on_response"
CONF_HEADERS = "headers" CONF_HEADERS = "headers"
CONF_COLLECT_HEADERS = "collect_headers" CONF_COLLECT_HEADERS = "collect_headers"
CONF_BODY = "body" CONF_BODY = "body"

View File

@@ -2,6 +2,7 @@
#include <vector> #include <vector>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT #define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT

View File

@@ -5,7 +5,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include <set> #include <initializer_list>
namespace esphome { namespace esphome {
namespace lock { namespace lock {
@@ -44,16 +44,22 @@ class LockTraits {
bool get_assumed_state() const { return this->assumed_state_; } bool get_assumed_state() const { return this->assumed_state_; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
bool supports_state(LockState state) const { return supported_states_.count(state); } bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); }
std::set<LockState> get_supported_states() const { return supported_states_; } void set_supported_states(std::initializer_list<LockState> states) {
void set_supported_states(std::set<LockState> states) { supported_states_ = std::move(states); } supported_states_mask_ = 0;
void add_supported_state(LockState state) { supported_states_.insert(state); } for (auto state : states) {
supported_states_mask_ |= (1 << state);
}
}
uint8_t get_supported_states_mask() const { return supported_states_mask_; }
void set_supported_states_mask(uint8_t mask) { supported_states_mask_ = mask; }
void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); }
protected: protected:
bool supports_open_{false}; bool supports_open_{false};
bool requires_code_{false}; bool requires_code_{false};
bool assumed_state_{false}; bool assumed_state_{false};
std::set<LockState> supported_states_ = {LOCK_STATE_NONE, LOCK_STATE_LOCKED, LOCK_STATE_UNLOCKED}; uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)};
}; };
/** This class is used to encode all control actions on a lock device. /** This class is used to encode all control actions on a lock device.

View File

@@ -95,6 +95,7 @@ DEFAULT = "DEFAULT"
CONF_INITIAL_LEVEL = "initial_level" CONF_INITIAL_LEVEL = "initial_level"
CONF_LOGGER_ID = "logger_id" CONF_LOGGER_ID = "logger_id"
CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels"
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
UART_SELECTION_ESP32 = { UART_SELECTION_ESP32 = {
@@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All(
} }
), ),
cv.Optional(CONF_INITIAL_LEVEL): is_log_level, cv.Optional(CONF_INITIAL_LEVEL): is_log_level,
cv.Optional(CONF_RUNTIME_TAG_LEVELS, default=False): cv.boolean,
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger),
@@ -291,8 +293,12 @@ async def to_code(config):
) )
cg.add(log.pre_setup()) cg.add(log.pre_setup())
for tag, log_level in config[CONF_LOGS].items(): # Enable runtime tag levels if logs are configured or explicitly enabled
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) logs_config = config[CONF_LOGS]
if logs_config or config[CONF_RUNTIME_TAG_LEVELS]:
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
for tag, log_level in logs_config.items():
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
cg.add_define("USE_LOGGER") cg.add_define("USE_LOGGER")
this_severity = LOG_LEVEL_SEVERITY.index(level) this_severity = LOG_LEVEL_SEVERITY.index(level)
@@ -443,6 +449,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
level = LOG_LEVELS[config[CONF_LEVEL]] level = LOG_LEVELS[config[CONF_LEVEL]]
logger = await cg.get_variable(config[CONF_LOGGER_ID]) logger = await cg.get_variable(config[CONF_LOGGER_ID])
if tag := config.get(CONF_TAG): if tag := config.get(CONF_TAG):
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
text = str(cg.statement(logger.set_log_level(tag, level))) text = str(cg.statement(logger.set_log_level(tag, level)))
else: else:
text = str(cg.statement(logger.set_log_level(level))) text = str(cg.statement(logger.set_log_level(level)))

View File

@@ -148,9 +148,11 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
#endif // USE_STORE_LOG_STR_IN_FLASH #endif // USE_STORE_LOG_STR_IN_FLASH
inline uint8_t Logger::level_for(const char *tag) { inline uint8_t Logger::level_for(const char *tag) {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
auto it = this->log_levels_.find(tag); auto it = this->log_levels_.find(tag);
if (it != this->log_levels_.end()) if (it != this->log_levels_.end())
return it->second; return it->second;
#endif
return this->current_level_; return this->current_level_;
} }
@@ -220,7 +222,9 @@ void Logger::process_messages_() {
} }
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
UARTSelection Logger::get_uart() const { return this->uart_; } UARTSelection Logger::get_uart() const { return this->uart_; }
@@ -271,9 +275,11 @@ void Logger::dump_config() {
} }
#endif #endif
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
for (auto &it : this->log_levels_) { for (auto &it : this->log_levels_) {
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second])); ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second]));
} }
#endif
} }
void Logger::set_log_level(uint8_t level) { void Logger::set_log_level(uint8_t level) {

View File

@@ -36,29 +36,38 @@ struct device;
namespace esphome::logger { namespace esphome::logger {
// Color and letter constants for log levels #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
static const char *const LOG_LEVEL_COLORS[] = { // Comparison function for const char* keys in log_levels_ map
"", // NONE struct CStrCompare {
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; }
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING };
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO #endif
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG // ANSI color code last digit (30-38 range, store only last digit to save RAM)
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE '\0', // NONE
'1', // ERROR (31 = red)
'3', // WARNING (33 = yellow)
'2', // INFO (32 = green)
'5', // CONFIG (35 = magenta)
'6', // DEBUG (36 = cyan)
'7', // VERBOSE (37 = gray)
'8', // VERY_VERBOSE (38 = white)
}; };
static const char *const LOG_LEVEL_LETTERS[] = { static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
"", // NONE '\0', // NONE
"E", // ERROR 'E', // ERROR
"W", // WARNING 'W', // WARNING
"I", // INFO 'I', // INFO
"C", // CONFIG 'C', // CONFIG
"D", // DEBUG 'D', // DEBUG
"V", // VERBOSE 'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
"VV", // VERY_VERBOSE
}; };
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection /** Enum for logging UART selection
* *
@@ -131,8 +140,10 @@ class Logger : public Component {
/// Set the default log level for this logger. /// Set the default log level for this logger.
void set_log_level(uint8_t level); void set_log_level(uint8_t level);
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
/// Set the log level of the specified tag. /// Set the log level of the specified tag.
void set_log_level(const std::string &tag, uint8_t log_level); void set_log_level(const char *tag, uint8_t log_level);
#endif
uint8_t get_log_level() { return this->current_level_; } uint8_t get_log_level() { return this->current_level_; }
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
@@ -215,14 +226,6 @@ class Logger : public Component {
} }
} }
// Format string to explicit buffer with varargs
inline void printf_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg);
va_end(arg);
}
#ifndef USE_HOST #ifndef USE_HOST
const LogString *get_uart_selection_(); const LogString *get_uart_selection_();
#endif #endif
@@ -248,7 +251,9 @@ class Logger : public Component {
#endif #endif
// Large objects (internally aligned) // Large objects (internally aligned)
std::map<std::string, uint8_t> log_levels_{}; #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
std::map<const char *, uint8_t, CStrCompare> log_levels_{};
#endif
CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{}; CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{};
CallbackManager<void(uint8_t)> level_callback_{}; CallbackManager<void(uint8_t)> level_callback_{};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
@@ -318,26 +323,76 @@ class Logger : public Component {
} }
#endif #endif
static inline void copy_string(char *buffer, uint16_t &pos, const char *str) {
const size_t len = strlen(str);
// Intentionally no null terminator, building larger string
memcpy(buffer + pos, str, len); // NOLINT(bugprone-not-null-terminated-result)
pos += len;
}
static inline void write_ansi_color_for_level(char *buffer, uint16_t &pos, uint8_t level) {
if (level == 0)
return;
// Construct ANSI escape sequence: "\033[{bold};3{color}m"
// Example: "\033[1;31m" for ERROR (bold red)
buffer[pos++] = '\033';
buffer[pos++] = '[';
buffer[pos++] = (level == 1) ? '1' : '0'; // Only ERROR is bold
buffer[pos++] = ';';
buffer[pos++] = '3';
buffer[pos++] = LOG_LEVEL_COLOR_DIGIT[level];
buffer[pos++] = 'm';
}
inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name,
char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
// Format header uint16_t pos = *buffer_at;
// uint8_t level is already bounded 0-255, just ensure it's <= 7 // Early return if insufficient space - intentionally don't update buffer_at to prevent partial writes
if (level > 7) if (pos + MAX_HEADER_SIZE > buffer_size)
level = 7; return;
const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; // Construct: <color>[LEVEL][tag:line]:
const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; write_ansi_color_for_level(buffer, pos, level);
buffer[pos++] = '[';
if (level != 0) {
if (level >= 7) {
buffer[pos++] = 'V'; // VERY_VERBOSE = "VV"
buffer[pos++] = 'V';
} else {
buffer[pos++] = LOG_LEVEL_LETTER_CHARS[level];
}
}
buffer[pos++] = ']';
buffer[pos++] = '[';
copy_string(buffer, pos, tag);
buffer[pos++] = ':';
// Format line number without modulo operations (passed by value, safe to mutate)
if (line > 999) [[unlikely]] {
int thousands = line / 1000;
buffer[pos++] = '0' + thousands;
line -= thousands * 1000;
}
int hundreds = line / 100;
int remainder = line - hundreds * 100;
int tens = remainder / 10;
buffer[pos++] = '0' + hundreds;
buffer[pos++] = '0' + tens;
buffer[pos++] = '0' + (remainder - tens * 10);
buffer[pos++] = ']';
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
if (thread_name != nullptr) { if (thread_name != nullptr) {
// Non-main task with thread name write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, buffer[pos++] = '[';
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color); copy_string(buffer, pos, thread_name);
return; buffer[pos++] = ']';
write_ansi_color_for_level(buffer, pos, level); // Restore original color
} }
#endif #endif
// Main task or non ESP32/LibreTiny platform
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line); buffer[pos++] = ':';
buffer[pos++] = ' ';
*buffer_at = pos;
} }
inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format,

View File

@@ -3,11 +3,10 @@
namespace esphome::logger { namespace esphome::logger {
void LoggerLevelSelect::publish_state(int level) { void LoggerLevelSelect::publish_state(int level) {
auto value = this->at(level); const auto &option = this->at(level_to_index(level));
if (!value) { if (!option)
return; return;
} Select::publish_state(option.value());
Select::publish_state(value.value());
} }
void LoggerLevelSelect::setup() { void LoggerLevelSelect::setup() {
@@ -16,10 +15,10 @@ void LoggerLevelSelect::setup() {
} }
void LoggerLevelSelect::control(const std::string &value) { void LoggerLevelSelect::control(const std::string &value) {
auto level = this->index_of(value); const auto index = this->index_of(value);
if (!level) if (!index)
return; return;
this->parent_->set_log_level(level.value()); this->parent_->set_log_level(index_to_level(index.value()));
} }
} // namespace esphome::logger } // namespace esphome::logger

View File

@@ -3,11 +3,18 @@
#include "esphome/components/select/select.h" #include "esphome/components/select/select.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
namespace esphome::logger { namespace esphome::logger {
class LoggerLevelSelect : public Component, public select::Select, public Parented<Logger> { class LoggerLevelSelect : public Component, public select::Select, public Parented<Logger> {
public: public:
void publish_state(int level); void publish_state(int level);
void setup() override; void setup() override;
void control(const std::string &value) override; void control(const std::string &value) override;
protected:
// Convert log level to option index (skip CONFIG at level 4)
static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; }
// Convert option index to log level (skip CONFIG at level 4)
static uint8_t index_to_level(uint8_t index) { return (index >= ESPHOME_LOG_LEVEL_CONFIG) ? index + 1 : index; }
}; };
} // namespace esphome::logger } // namespace esphome::logger

View File

@@ -155,7 +155,7 @@ void MCP2515::prepare_id_(uint8_t *buffer, const bool extended, const uint32_t i
canid = (uint16_t) (id >> 16); canid = (uint16_t) (id >> 16);
buffer[MCP_SIDL] = (uint8_t) (canid & 0x03); buffer[MCP_SIDL] = (uint8_t) (canid & 0x03);
buffer[MCP_SIDL] += (uint8_t) ((canid & 0x1C) << 3); buffer[MCP_SIDL] += (uint8_t) ((canid & 0x1C) << 3);
buffer[MCP_SIDL] |= TXB_EXIDE_MASK; buffer[MCP_SIDL] |= SIDL_EXIDE_MASK;
buffer[MCP_SIDH] = (uint8_t) (canid >> 5); buffer[MCP_SIDH] = (uint8_t) (canid >> 5);
} else { } else {
buffer[MCP_SIDH] = (uint8_t) (canid >> 3); buffer[MCP_SIDH] = (uint8_t) (canid >> 3);
@@ -258,7 +258,7 @@ canbus::Error MCP2515::send_message(struct canbus::CanFrame *frame) {
} }
} }
return canbus::ERROR_FAILTX; return canbus::ERROR_ALLTXBUSY;
} }
canbus::Error MCP2515::read_message_(RXBn rxbn, struct canbus::CanFrame *frame) { canbus::Error MCP2515::read_message_(RXBn rxbn, struct canbus::CanFrame *frame) {
@@ -272,7 +272,7 @@ canbus::Error MCP2515::read_message_(RXBn rxbn, struct canbus::CanFrame *frame)
bool use_extended_id = false; bool use_extended_id = false;
bool remote_transmission_request = false; bool remote_transmission_request = false;
if ((tbufdata[MCP_SIDL] & TXB_EXIDE_MASK) == TXB_EXIDE_MASK) { if ((tbufdata[MCP_SIDL] & SIDL_EXIDE_MASK) == SIDL_EXIDE_MASK) {
id = (id << 2) + (tbufdata[MCP_SIDL] & 0x03); id = (id << 2) + (tbufdata[MCP_SIDL] & 0x03);
id = (id << 8) + tbufdata[MCP_EID8]; id = (id << 8) + tbufdata[MCP_EID8];
id = (id << 8) + tbufdata[MCP_EID0]; id = (id << 8) + tbufdata[MCP_EID0];
@@ -315,6 +315,17 @@ canbus::Error MCP2515::read_message(struct canbus::CanFrame *frame) {
rc = canbus::ERROR_NOMSG; rc = canbus::ERROR_NOMSG;
} }
#ifdef ESPHOME_LOG_HAS_DEBUG
uint8_t err = get_error_flags_();
// The receive flowchart in the datasheet says that if rollover is set (BUKT), RX1OVR flag will be set
// once both buffers are full. However, the RX0OVR flag is actually set instead.
// We can just check for both though because it doesn't break anything.
if (err & (EFLG_RX0OVR | EFLG_RX1OVR)) {
ESP_LOGD(TAG, "receive buffer overrun");
clear_rx_n_ovr_flags_();
}
#endif
return rc; return rc;
} }

View File

@@ -130,7 +130,9 @@ static const uint8_t CANSTAT_ICOD = 0x0E;
static const uint8_t CNF3_SOF = 0x80; static const uint8_t CNF3_SOF = 0x80;
static const uint8_t TXB_EXIDE_MASK = 0x08; // applies to RXBn_SIDL, TXBn_SIDL and RXFn_SIDL
static const uint8_t SIDL_EXIDE_MASK = 0x08;
static const uint8_t DLC_MASK = 0x0F; static const uint8_t DLC_MASK = 0x0F;
static const uint8_t RTR_MASK = 0x40; static const uint8_t RTR_MASK = 0x40;

View File

@@ -17,6 +17,11 @@ from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
# Components that create mDNS services at runtime
# IMPORTANT: If you add a new component here, you must also update the corresponding
# #ifdef blocks in mdns_component.cpp compile_records_() method
COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "web_server")
mdns_ns = cg.esphome_ns.namespace("mdns") mdns_ns = cg.esphome_ns.namespace("mdns")
MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component)
MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord") MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord")
@@ -91,12 +96,20 @@ async def to_code(config):
cg.add_define("USE_MDNS") cg.add_define("USE_MDNS")
var = cg.new_Pvariable(config[CONF_ID]) # Calculate compile-time service count
await cg.register_component(var, config) service_count = sum(
1 for key in COMPONENTS_WITH_MDNS_SERVICES if key in CORE.config
) + len(config[CONF_SERVICES])
if config[CONF_SERVICES]: if config[CONF_SERVICES]:
cg.add_define("USE_MDNS_EXTRA_SERVICES") cg.add_define("USE_MDNS_EXTRA_SERVICES")
# Ensure at least 1 service (fallback service)
cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for service in config[CONF_SERVICES]: for service in config[CONF_SERVICES]:
txt = [ txt = [
cg.StructInitializer( cg.StructInitializer(

View File

@@ -74,32 +74,12 @@ MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
void MDNSComponent::compile_records_() { void MDNSComponent::compile_records_() {
this->hostname_ = App.get_name(); this->hostname_ = App.get_name();
// Calculate exact capacity needed for services vector // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES
size_t services_count = 0; // in mdns/__init__.py. If you add a new service here, update both locations.
#ifdef USE_API
if (api::global_api_server != nullptr) {
services_count++;
}
#endif
#ifdef USE_PROMETHEUS
services_count++;
#endif
#ifdef USE_WEBSERVER
services_count++;
#endif
#ifdef USE_MDNS_EXTRA_SERVICES
services_count += this->services_extra_.size();
#endif
// Reserve for fallback service if needed
if (services_count == 0) {
services_count = 1;
}
this->services_.reserve(services_count);
#ifdef USE_API #ifdef USE_API
if (api::global_api_server != nullptr) { if (api::global_api_server != nullptr) {
this->services_.emplace_back(); auto &service = this->services_.emplace_next();
auto &service = this->services_.back();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP); service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_port(); service.port = api::global_api_server->get_port();
@@ -178,30 +158,23 @@ void MDNSComponent::compile_records_() {
#endif // USE_API #endif // USE_API
#ifdef USE_PROMETHEUS #ifdef USE_PROMETHEUS
this->services_.emplace_back(); auto &prom_service = this->services_.emplace_next();
auto &prom_service = this->services_.back();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT; prom_service.port = USE_WEBSERVER_PORT;
#endif #endif
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
this->services_.emplace_back(); auto &web_service = this->services_.emplace_next();
auto &web_service = this->services_.back();
web_service.service_type = MDNS_STR(SERVICE_HTTP); web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP); web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT; web_service.port = USE_WEBSERVER_PORT;
#endif #endif
#ifdef USE_MDNS_EXTRA_SERVICES
this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end());
#endif
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES)
// Publish "http" service if not using native API or any other services // Publish "http" service if not using native API or any other services
// This is just to have *some* mDNS service so that .local resolution works // This is just to have *some* mDNS service so that .local resolution works
this->services_.emplace_back(); auto &fallback_service = this->services_.emplace_next();
auto &fallback_service = this->services_.back();
fallback_service.service_type = "_http"; fallback_service.service_type = "_http";
fallback_service.proto = "_tcp"; fallback_service.proto = "_tcp";
fallback_service.port = USE_WEBSERVER_PORT; fallback_service.port = USE_WEBSERVER_PORT;
@@ -214,7 +187,7 @@ void MDNSComponent::dump_config() {
"mDNS:\n" "mDNS:\n"
" Hostname: %s", " Hostname: %s",
this->hostname_.c_str()); this->hostname_.c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, " Services:"); ESP_LOGV(TAG, " Services:");
for (const auto &service : this->services_) { for (const auto &service : this->services_) {
ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(),
@@ -227,8 +200,6 @@ void MDNSComponent::dump_config() {
#endif #endif
} }
std::vector<MDNSService> MDNSComponent::get_services() { return this->services_; }
} // namespace mdns } // namespace mdns
} // namespace esphome } // namespace esphome
#endif #endif

View File

@@ -2,13 +2,16 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_MDNS #ifdef USE_MDNS
#include <string> #include <string>
#include <vector>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
namespace mdns { namespace mdns {
// Service count is calculated at compile time by Python codegen
// MDNS_SERVICE_COUNT will always be defined
struct MDNSTXTRecord { struct MDNSTXTRecord {
std::string key; std::string key;
TemplatableValue<std::string> value; TemplatableValue<std::string> value;
@@ -36,18 +39,15 @@ class MDNSComponent : public Component {
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
#ifdef USE_MDNS_EXTRA_SERVICES #ifdef USE_MDNS_EXTRA_SERVICES
void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); } void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); }
#endif #endif
std::vector<MDNSService> get_services(); const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; }
void on_shutdown() override; void on_shutdown() override;
protected: protected:
#ifdef USE_MDNS_EXTRA_SERVICES StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
std::vector<MDNSService> services_extra_{};
#endif
std::vector<MDNSService> services_{};
std::string hostname_; std::string hostname_;
void compile_records_(); void compile_records_();
}; };

View File

@@ -343,11 +343,7 @@ class DriverChip:
) )
offset_height = native_height - height - offset_height offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer # Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer
rotated = not requires_buffer(config) and config.get(CONF_ROTATION, 0) in ( if transform.get(CONF_SWAP_XY) is True:
90,
270,
)
if transform.get(CONF_SWAP_XY) is True or rotated:
width, height = height, width width, height = height, width
offset_height, offset_width = offset_width, offset_height offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height return width, height, offset_width, offset_height

View File

@@ -380,25 +380,41 @@ def get_instance(config):
bus_type = BusTypes[bus_type] bus_type = BusTypes[bus_type]
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config) frac = denominator(config)
rotation = DISPLAY_ROTATIONS[ rotation = (
0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0) 0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0)
] )
templateargs = [ templateargs = [
buffer_type, buffer_type,
bufferpixels, bufferpixels,
config[CONF_BYTE_ORDER] == "big_endian", config[CONF_BYTE_ORDER] == "big_endian",
display_pixel_mode, display_pixel_mode,
bus_type, bus_type,
width,
height,
offset_width,
offset_height,
] ]
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
if requires_buffer(config): if requires_buffer(config):
templateargs.append(rotation) templateargs.extend(
templateargs.append(frac) [
width,
height,
offset_width,
offset_height,
DISPLAY_ROTATIONS[rotation],
frac,
]
)
return MipiSpiBuffer, templateargs return MipiSpiBuffer, templateargs
# Swap height and width if the display is rotated 90 or 270 degrees in software
if rotation in (90, 270):
width, height = height, width
offset_width, offset_height = offset_height, offset_width
templateargs.extend(
[
width,
height,
offset_width,
offset_height,
]
)
return MipiSpi, templateargs return MipiSpi, templateargs

View File

@@ -11,47 +11,49 @@ namespace mpr121 {
static const char *const TAG = "mpr121"; static const char *const TAG = "mpr121";
void MPR121Component::setup() { void MPR121Component::setup() {
this->disable_loop();
// soft reset device // soft reset device
this->write_byte(MPR121_SOFTRESET, 0x63); this->write_byte(MPR121_SOFTRESET, 0x63);
delay(100); // NOLINT this->set_timeout(100, [this]() {
if (!this->write_byte(MPR121_ECR, 0x0)) { if (!this->write_byte(MPR121_ECR, 0x0)) {
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(); this->mark_failed();
return; return;
} }
// set touch sensitivity for all 12 channels
for (auto *channel : this->channels_) {
channel->setup();
}
this->write_byte(MPR121_MHDR, 0x01);
this->write_byte(MPR121_NHDR, 0x01);
this->write_byte(MPR121_NCLR, 0x0E);
this->write_byte(MPR121_FDLR, 0x00);
// set touch sensitivity for all 12 channels this->write_byte(MPR121_MHDF, 0x01);
for (auto *channel : this->channels_) { this->write_byte(MPR121_NHDF, 0x05);
channel->setup(); this->write_byte(MPR121_NCLF, 0x01);
} this->write_byte(MPR121_FDLF, 0x00);
this->write_byte(MPR121_MHDR, 0x01);
this->write_byte(MPR121_NHDR, 0x01);
this->write_byte(MPR121_NCLR, 0x0E);
this->write_byte(MPR121_FDLR, 0x00);
this->write_byte(MPR121_MHDF, 0x01); this->write_byte(MPR121_NHDT, 0x00);
this->write_byte(MPR121_NHDF, 0x05); this->write_byte(MPR121_NCLT, 0x00);
this->write_byte(MPR121_NCLF, 0x01); this->write_byte(MPR121_FDLT, 0x00);
this->write_byte(MPR121_FDLF, 0x00);
this->write_byte(MPR121_NHDT, 0x00); this->write_byte(MPR121_DEBOUNCE, 0);
this->write_byte(MPR121_NCLT, 0x00); // default, 16uA charge current
this->write_byte(MPR121_FDLT, 0x00); this->write_byte(MPR121_CONFIG1, 0x10);
// 0.5uS encoding, 1ms period
this->write_byte(MPR121_CONFIG2, 0x20);
this->write_byte(MPR121_DEBOUNCE, 0); // Write the Electrode Configuration Register
// default, 16uA charge current // * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits.
this->write_byte(MPR121_CONFIG1, 0x10); // * The 2 bits below is "Proximity Enable" and are left at 0.
// 0.5uS encoding, 1ms period // * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled
this->write_byte(MPR121_CONFIG2, 0x20); // as a range, starting at 0 up to the highest channel index used.
this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1));
// Write the Electrode Configuration Register this->flush_gpio_();
// * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits. this->enable_loop();
// * The 2 bits below is "Proximity Enable" and are left at 0. });
// * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled
// as a range, starting at 0 up to the highest channel index used.
this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1));
this->flush_gpio_();
} }
void MPR121Component::set_touch_debounce(uint8_t debounce) { void MPR121Component::set_touch_debounce(uint8_t debounce) {
@@ -73,9 +75,6 @@ void MPR121Component::dump_config() {
case COMMUNICATION_FAILED: case COMMUNICATION_FAILED:
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
break; break;
case WRONG_CHIP_STATE:
ESP_LOGE(TAG, "MPR121 has wrong default value for CONFIG2?");
break;
case NONE: case NONE:
default: default:
break; break;

View File

@@ -88,7 +88,6 @@ class MPR121Component : public Component, public i2c::I2CDevice {
enum ErrorCode { enum ErrorCode {
NONE = 0, NONE = 0,
COMMUNICATION_FAILED, COMMUNICATION_FAILED,
WRONG_CHIP_STATE,
} error_code_{NONE}; } error_code_{NONE};
bool flush_gpio_(); bool flush_gpio_();

View File

@@ -77,7 +77,7 @@ bool Nextion::check_connect_() {
this->recv_ret_string_(response, 0, false); this->recv_ret_string_(response, 0, false);
if (!response.empty() && response[0] == 0x1A) { if (!response.empty() && response[0] == 0x1A) {
// Swallow invalid variable name responses that may be caused by the above commands // Swallow invalid variable name responses that may be caused by the above commands
ESP_LOGD(TAG, "0x1A error ignored (setup)"); ESP_LOGV(TAG, "0x1A error ignored (setup)");
return false; return false;
} }
if (response.empty() || response.find("comok") == std::string::npos) { if (response.empty() || response.find("comok") == std::string::npos) {
@@ -334,7 +334,7 @@ void Nextion::loop() {
this->started_ms_ = App.get_loop_component_start_time(); this->started_ms_ = App.get_loop_component_start_time();
if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
ESP_LOGD(TAG, "Manual ready set"); ESP_LOGV(TAG, "Manual ready set");
this->connection_state_.nextion_reports_is_setup_ = true; this->connection_state_.nextion_reports_is_setup_ = true;
} }
} }
@@ -544,7 +544,7 @@ void Nextion::process_nextion_commands_() {
uint8_t page_id = to_process[0]; uint8_t page_id = to_process[0];
uint8_t component_id = to_process[1]; uint8_t component_id = to_process[1];
uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press
ESP_LOGD(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id); ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id);
for (auto *touch : this->touch_) { for (auto *touch : this->touch_) {
touch->process_touch(page_id, component_id, touch_event != 0); touch->process_touch(page_id, component_id, touch_event != 0);
} }
@@ -559,7 +559,7 @@ void Nextion::process_nextion_commands_() {
} }
uint8_t page_id = to_process[0]; uint8_t page_id = to_process[0];
ESP_LOGD(TAG, "New page: %u", page_id); ESP_LOGV(TAG, "New page: %u", page_id);
this->page_callback_.call(page_id); this->page_callback_.call(page_id);
break; break;
} }
@@ -577,7 +577,7 @@ void Nextion::process_nextion_commands_() {
const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press
ESP_LOGD(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y); ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y);
break; break;
} }
@@ -676,7 +676,7 @@ void Nextion::process_nextion_commands_() {
} }
case 0x88: // system successful start up case 0x88: // system successful start up
{ {
ESP_LOGD(TAG, "System start: %zu", to_process_length); ESP_LOGV(TAG, "System start: %zu", to_process_length);
this->connection_state_.nextion_reports_is_setup_ = true; this->connection_state_.nextion_reports_is_setup_ = true;
break; break;
} }
@@ -922,7 +922,7 @@ void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::s
} }
void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) {
ESP_LOGD(TAG, "State: %s='%s'", name.c_str(), state.c_str()); ESP_LOGV(TAG, "State: %s='%s'", name.c_str(), state.c_str());
for (auto *sensor : this->textsensortype_) { for (auto *sensor : this->textsensortype_) {
if (name == sensor->get_variable_name()) { if (name == sensor->get_variable_name()) {
@@ -933,7 +933,7 @@ void Nextion::set_nextion_text_state(const std::string &name, const std::string
} }
void Nextion::all_components_send_state_(bool force_update) { void Nextion::all_components_send_state_(bool force_update) {
ESP_LOGD(TAG, "Send states"); ESP_LOGV(TAG, "Send states");
for (auto *binarysensortype : this->binarysensortype_) { for (auto *binarysensortype : this->binarysensortype_) {
if (force_update || binarysensortype->get_needs_to_send_update()) if (force_update || binarysensortype->get_needs_to_send_update())
binarysensortype->send_state_to_nextion(); binarysensortype->send_state_to_nextion();

View File

@@ -7,6 +7,17 @@ namespace number {
static const char *const TAG = "number"; static const char *const TAG = "number";
// Helper functions to reduce code size for logging
void NumberCall::log_perform_warning_(const LogString *message) {
ESP_LOGW(TAG, "'%s': %s", this->parent_->get_name().c_str(), LOG_STR_ARG(message));
}
void NumberCall::log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val,
float limit) {
ESP_LOGW(TAG, "'%s': %f %s %s %f", this->parent_->get_name().c_str(), val, LOG_STR_ARG(comparison),
LOG_STR_ARG(limit_type), limit);
}
NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); }
NumberCall &NumberCall::number_increment(bool cycle) { NumberCall &NumberCall::number_increment(bool cycle) {
@@ -42,7 +53,7 @@ void NumberCall::perform() {
const auto &traits = parent->traits; const auto &traits = parent->traits;
if (this->operation_ == NUMBER_OP_NONE) { if (this->operation_ == NUMBER_OP_NONE) {
ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); this->log_perform_warning_(LOG_STR("No operation"));
return; return;
} }
@@ -51,28 +62,28 @@ void NumberCall::perform() {
float max_value = traits.get_max_value(); float max_value = traits.get_max_value();
if (this->operation_ == NUMBER_OP_SET) { if (this->operation_ == NUMBER_OP_SET) {
ESP_LOGD(TAG, "'%s' - Setting number value", name); ESP_LOGD(TAG, "'%s': Setting value", name);
if (!this->value_.has_value() || std::isnan(*this->value_)) { if (!this->value_.has_value() || std::isnan(*this->value_)) {
ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); this->log_perform_warning_(LOG_STR("No value"));
return; return;
} }
target_value = this->value_.value(); target_value = this->value_.value();
} else if (this->operation_ == NUMBER_OP_TO_MIN) { } else if (this->operation_ == NUMBER_OP_TO_MIN) {
if (std::isnan(min_value)) { if (std::isnan(min_value)) {
ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); this->log_perform_warning_(LOG_STR("min undefined"));
} else { } else {
target_value = min_value; target_value = min_value;
} }
} else if (this->operation_ == NUMBER_OP_TO_MAX) { } else if (this->operation_ == NUMBER_OP_TO_MAX) {
if (std::isnan(max_value)) { if (std::isnan(max_value)) {
ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); this->log_perform_warning_(LOG_STR("max undefined"));
} else { } else {
target_value = max_value; target_value = max_value;
} }
} else if (this->operation_ == NUMBER_OP_INCREMENT) { } else if (this->operation_ == NUMBER_OP_INCREMENT) {
ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out");
if (!parent->has_state()) { if (!parent->has_state()) {
ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); this->log_perform_warning_(LOG_STR("Can't increment, no state"));
return; return;
} }
auto step = traits.get_step(); auto step = traits.get_step();
@@ -85,9 +96,9 @@ void NumberCall::perform() {
} }
} }
} else if (this->operation_ == NUMBER_OP_DECREMENT) { } else if (this->operation_ == NUMBER_OP_DECREMENT) {
ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out");
if (!parent->has_state()) { if (!parent->has_state()) {
ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); this->log_perform_warning_(LOG_STR("Can't decrement, no state"));
return; return;
} }
auto step = traits.get_step(); auto step = traits.get_step();
@@ -102,15 +113,15 @@ void NumberCall::perform() {
} }
if (target_value < min_value) { if (target_value < min_value) {
ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); this->log_perform_warning_value_range_(LOG_STR("<"), LOG_STR("min"), target_value, min_value);
return; return;
} }
if (target_value > max_value) { if (target_value > max_value) {
ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); this->log_perform_warning_value_range_(LOG_STR(">"), LOG_STR("max"), target_value, max_value);
return; return;
} }
ESP_LOGD(TAG, " New number value: %f", target_value); ESP_LOGD(TAG, " New value: %f", target_value);
this->parent_->control(target_value); this->parent_->control(target_value);
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "number_traits.h" #include "number_traits.h"
namespace esphome { namespace esphome {
@@ -33,6 +34,10 @@ class NumberCall {
NumberCall &with_cycle(bool cycle); NumberCall &with_cycle(bool cycle);
protected: protected:
void log_perform_warning_(const LogString *message);
void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val,
float limit);
Number *const parent_; Number *const parent_;
NumberOperation operation_{NUMBER_OP_NONE}; NumberOperation operation_{NUMBER_OP_NONE};
optional<float> value_; optional<float> value_;

View File

@@ -143,11 +143,10 @@ void OpenThreadSrpComponent::setup() {
return; return;
} }
// Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this // Get mdns services and copy their data (strings are copied with strdup below)
// component const auto &mdns_services = this->mdns_->get_services();
this->mdns_services_ = this->mdns_->get_services(); ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size());
ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); for (const auto &service : mdns_services) {
for (const auto &service : this->mdns_services_) {
otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance);
if (!entry) { if (!entry) {
ESP_LOGW(TAG, "Failed to allocate service entry"); ESP_LOGW(TAG, "Failed to allocate service entry");

View File

@@ -57,7 +57,6 @@ class OpenThreadSrpComponent : public Component {
protected: protected:
esphome::mdns::MDNSComponent *mdns_{nullptr}; esphome::mdns::MDNSComponent *mdns_{nullptr};
std::vector<esphome::mdns::MDNSService> mdns_services_;
std::vector<std::unique_ptr<uint8_t[]>> memory_pool_; std::vector<std::unique_ptr<uint8_t[]>> memory_pool_;
void *pool_alloc_(size_t size); void *pool_alloc_(size_t size);
}; };

View File

@@ -110,21 +110,21 @@ std::string PrometheusHandler::relabel_name_(EntityBase *obj) {
void PrometheusHandler::add_area_label_(AsyncResponseStream *stream, std::string &area) { void PrometheusHandler::add_area_label_(AsyncResponseStream *stream, std::string &area) {
if (!area.empty()) { if (!area.empty()) {
stream->print(F("\",area=\"")); stream->print(ESPHOME_F("\",area=\""));
stream->print(area.c_str()); stream->print(area.c_str());
} }
} }
void PrometheusHandler::add_node_label_(AsyncResponseStream *stream, std::string &node) { void PrometheusHandler::add_node_label_(AsyncResponseStream *stream, std::string &node) {
if (!node.empty()) { if (!node.empty()) {
stream->print(F("\",node=\"")); stream->print(ESPHOME_F("\",node=\""));
stream->print(node.c_str()); stream->print(node.c_str());
} }
} }
void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name) { void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name) {
if (!friendly_name.empty()) { if (!friendly_name.empty()) {
stream->print(F("\",friendly_name=\"")); stream->print(ESPHOME_F("\",friendly_name=\""));
stream->print(friendly_name.c_str()); stream->print(friendly_name.c_str());
} }
} }
@@ -132,8 +132,8 @@ void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, st
// Type-specific implementation // Type-specific implementation
#ifdef USE_SENSOR #ifdef USE_SENSOR
void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_sensor_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_sensor_value gauge\n"));
stream->print(F("#TYPE esphome_sensor_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_sensor_failed gauge\n"));
} }
void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area,
std::string &node, std::string &friendly_name) { std::string &node, std::string &friendly_name) {
@@ -141,37 +141,37 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor
return; return;
if (!std::isnan(obj->state)) { if (!std::isnan(obj->state)) {
// We have a valid value, output this value // We have a valid value, output this value
stream->print(F("esphome_sensor_failed{id=\"")); stream->print(ESPHOME_F("esphome_sensor_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_sensor_value{id=\"")); stream->print(ESPHOME_F("esphome_sensor_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",unit=\"")); stream->print(ESPHOME_F("\",unit=\""));
stream->print(obj->get_unit_of_measurement().c_str()); stream->print(obj->get_unit_of_measurement().c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str());
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} else { } else {
// Invalid state // Invalid state
stream->print(F("esphome_sensor_failed{id=\"")); stream->print(ESPHOME_F("esphome_sensor_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n")); stream->print(ESPHOME_F("\"} 1\n"));
} }
} }
#endif #endif
@@ -179,8 +179,8 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor
// Type-specific implementation // Type-specific implementation
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_binary_sensor_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_binary_sensor_value gauge\n"));
stream->print(F("#TYPE esphome_binary_sensor_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_binary_sensor_failed gauge\n"));
} }
void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj, void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj,
std::string &area, std::string &node, std::string &friendly_name) { std::string &area, std::string &node, std::string &friendly_name) {
@@ -188,204 +188,204 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s
return; return;
if (obj->has_state()) { if (obj->has_state()) {
// We have a valid value, output this value // We have a valid value, output this value
stream->print(F("esphome_binary_sensor_failed{id=\"")); stream->print(ESPHOME_F("esphome_binary_sensor_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_binary_sensor_value{id=\"")); stream->print(ESPHOME_F("esphome_binary_sensor_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->state); stream->print(obj->state);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} else { } else {
// Invalid state // Invalid state
stream->print(F("esphome_binary_sensor_failed{id=\"")); stream->print(ESPHOME_F("esphome_binary_sensor_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n")); stream->print(ESPHOME_F("\"} 1\n"));
} }
} }
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { void PrometheusHandler::fan_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_fan_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_fan_value gauge\n"));
stream->print(F("#TYPE esphome_fan_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_fan_failed gauge\n"));
stream->print(F("#TYPE esphome_fan_speed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_fan_speed gauge\n"));
stream->print(F("#TYPE esphome_fan_oscillation gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_fan_oscillation gauge\n"));
} }
void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node, void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node,
std::string &friendly_name) { std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_) if (obj->is_internal() && !this->include_internal_)
return; return;
stream->print(F("esphome_fan_failed{id=\"")); stream->print(ESPHOME_F("esphome_fan_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_fan_value{id=\"")); stream->print(ESPHOME_F("esphome_fan_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->state); stream->print(obj->state);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
// Speed if available // Speed if available
if (obj->get_traits().supports_speed()) { if (obj->get_traits().supports_speed()) {
stream->print(F("esphome_fan_speed{id=\"")); stream->print(ESPHOME_F("esphome_fan_speed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->speed); stream->print(obj->speed);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
// Oscillation if available // Oscillation if available
if (obj->get_traits().supports_oscillation()) { if (obj->get_traits().supports_oscillation()) {
stream->print(F("esphome_fan_oscillation{id=\"")); stream->print(ESPHOME_F("esphome_fan_oscillation{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->oscillating); stream->print(obj->oscillating);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
} }
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
void PrometheusHandler::light_type_(AsyncResponseStream *stream) { void PrometheusHandler::light_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_light_state gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_light_state gauge\n"));
stream->print(F("#TYPE esphome_light_color gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_light_color gauge\n"));
stream->print(F("#TYPE esphome_light_effect_active gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_light_effect_active gauge\n"));
} }
void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area,
std::string &node, std::string &friendly_name) { std::string &node, std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_) if (obj->is_internal() && !this->include_internal_)
return; return;
// State // State
stream->print(F("esphome_light_state{id=\"")); stream->print(ESPHOME_F("esphome_light_state{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->remote_values.is_on()); stream->print(obj->remote_values.is_on());
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
// Brightness and RGBW // Brightness and RGBW
light::LightColorValues color = obj->current_values; light::LightColorValues color = obj->current_values;
float brightness, r, g, b, w; float brightness, r, g, b, w;
color.as_brightness(&brightness); color.as_brightness(&brightness);
color.as_rgbw(&r, &g, &b, &w); color.as_rgbw(&r, &g, &b, &w);
stream->print(F("esphome_light_color{id=\"")); stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",channel=\"brightness\"} ")); stream->print(ESPHOME_F("\",channel=\"brightness\"} "));
stream->print(brightness); stream->print(brightness);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
stream->print(F("esphome_light_color{id=\"")); stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",channel=\"r\"} ")); stream->print(ESPHOME_F("\",channel=\"r\"} "));
stream->print(r); stream->print(r);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
stream->print(F("esphome_light_color{id=\"")); stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",channel=\"g\"} ")); stream->print(ESPHOME_F("\",channel=\"g\"} "));
stream->print(g); stream->print(g);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
stream->print(F("esphome_light_color{id=\"")); stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",channel=\"b\"} ")); stream->print(ESPHOME_F("\",channel=\"b\"} "));
stream->print(b); stream->print(b);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
stream->print(F("esphome_light_color{id=\"")); stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",channel=\"w\"} ")); stream->print(ESPHOME_F("\",channel=\"w\"} "));
stream->print(w); stream->print(w);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
// Effect // Effect
std::string effect = obj->get_effect_name(); std::string effect = obj->get_effect_name();
if (effect == "None") { if (effect == "None") {
stream->print(F("esphome_light_effect_active{id=\"")); stream->print(ESPHOME_F("esphome_light_effect_active{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",effect=\"None\"} 0\n")); stream->print(ESPHOME_F("\",effect=\"None\"} 0\n"));
} else { } else {
stream->print(F("esphome_light_effect_active{id=\"")); stream->print(ESPHOME_F("esphome_light_effect_active{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",effect=\"")); stream->print(ESPHOME_F("\",effect=\""));
stream->print(effect.c_str()); stream->print(effect.c_str());
stream->print(F("\"} 1\n")); stream->print(ESPHOME_F("\"} 1\n"));
} }
} }
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { void PrometheusHandler::cover_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_cover_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_cover_value gauge\n"));
stream->print(F("#TYPE esphome_cover_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_cover_failed gauge\n"));
} }
void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node, void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node,
std::string &friendly_name) { std::string &friendly_name) {
@@ -393,118 +393,118 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob
return; return;
if (!std::isnan(obj->position)) { if (!std::isnan(obj->position)) {
// We have a valid value, output this value // We have a valid value, output this value
stream->print(F("esphome_cover_failed{id=\"")); stream->print(ESPHOME_F("esphome_cover_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_cover_value{id=\"")); stream->print(ESPHOME_F("esphome_cover_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->position); stream->print(obj->position);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
if (obj->get_traits().get_supports_tilt()) { if (obj->get_traits().get_supports_tilt()) {
stream->print(F("esphome_cover_tilt{id=\"")); stream->print(ESPHOME_F("esphome_cover_tilt{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->tilt); stream->print(obj->tilt);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
} else { } else {
// Invalid state // Invalid state
stream->print(F("esphome_cover_failed{id=\"")); stream->print(ESPHOME_F("esphome_cover_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n")); stream->print(ESPHOME_F("\"} 1\n"));
} }
} }
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { void PrometheusHandler::switch_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_switch_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_switch_value gauge\n"));
stream->print(F("#TYPE esphome_switch_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_switch_failed gauge\n"));
} }
void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area,
std::string &node, std::string &friendly_name) { std::string &node, std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_) if (obj->is_internal() && !this->include_internal_)
return; return;
stream->print(F("esphome_switch_failed{id=\"")); stream->print(ESPHOME_F("esphome_switch_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_switch_value{id=\"")); stream->print(ESPHOME_F("esphome_switch_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->state); stream->print(obj->state);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { void PrometheusHandler::lock_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_lock_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_lock_value gauge\n"));
stream->print(F("#TYPE esphome_lock_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_lock_failed gauge\n"));
} }
void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node, void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node,
std::string &friendly_name) { std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_) if (obj->is_internal() && !this->include_internal_)
return; return;
stream->print(F("esphome_lock_failed{id=\"")); stream->print(ESPHOME_F("esphome_lock_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_lock_value{id=\"")); stream->print(ESPHOME_F("esphome_lock_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->state); stream->print(obj->state);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
#endif #endif
// Type-specific implementation // Type-specific implementation
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) { void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_text_sensor_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_text_sensor_value gauge\n"));
stream->print(F("#TYPE esphome_text_sensor_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_text_sensor_failed gauge\n"));
} }
void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area, void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area,
std::string &node, std::string &friendly_name) { std::string &node, std::string &friendly_name) {
@@ -512,37 +512,37 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
return; return;
if (obj->has_state()) { if (obj->has_state()) {
// We have a valid value, output this value // We have a valid value, output this value
stream->print(F("esphome_text_sensor_failed{id=\"")); stream->print(ESPHOME_F("esphome_text_sensor_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_text_sensor_value{id=\"")); stream->print(ESPHOME_F("esphome_text_sensor_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",value=\"")); stream->print(ESPHOME_F("\",value=\""));
stream->print(obj->state.c_str()); stream->print(obj->state.c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} else { } else {
// Invalid state // Invalid state
stream->print(F("esphome_text_sensor_failed{id=\"")); stream->print(ESPHOME_F("esphome_text_sensor_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n")); stream->print(ESPHOME_F("\"} 1\n"));
} }
} }
#endif #endif
@@ -550,8 +550,8 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
// Type-specific implementation // Type-specific implementation
#ifdef USE_NUMBER #ifdef USE_NUMBER
void PrometheusHandler::number_type_(AsyncResponseStream *stream) { void PrometheusHandler::number_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_number_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_number_value gauge\n"));
stream->print(F("#TYPE esphome_number_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_number_failed gauge\n"));
} }
void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area,
std::string &node, std::string &friendly_name) { std::string &node, std::string &friendly_name) {
@@ -559,43 +559,43 @@ void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number
return; return;
if (!std::isnan(obj->state)) { if (!std::isnan(obj->state)) {
// We have a valid value, output this value // We have a valid value, output this value
stream->print(F("esphome_number_failed{id=\"")); stream->print(ESPHOME_F("esphome_number_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_number_value{id=\"")); stream->print(ESPHOME_F("esphome_number_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->state); stream->print(obj->state);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} else { } else {
// Invalid state // Invalid state
stream->print(F("esphome_number_failed{id=\"")); stream->print(ESPHOME_F("esphome_number_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n")); stream->print(ESPHOME_F("\"} 1\n"));
} }
} }
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
void PrometheusHandler::select_type_(AsyncResponseStream *stream) { void PrometheusHandler::select_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_select_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_select_value gauge\n"));
stream->print(F("#TYPE esphome_select_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_select_failed gauge\n"));
} }
void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area,
std::string &node, std::string &friendly_name) { std::string &node, std::string &friendly_name) {
@@ -603,105 +603,105 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select
return; return;
if (obj->has_state()) { if (obj->has_state()) {
// We have a valid value, output this value // We have a valid value, output this value
stream->print(F("esphome_select_failed{id=\"")); stream->print(ESPHOME_F("esphome_select_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_select_value{id=\"")); stream->print(ESPHOME_F("esphome_select_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",value=\"")); stream->print(ESPHOME_F("\",value=\""));
stream->print(obj->state.c_str()); stream->print(obj->state.c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} else { } else {
// Invalid state // Invalid state
stream->print(F("esphome_select_failed{id=\"")); stream->print(ESPHOME_F("esphome_select_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n")); stream->print(ESPHOME_F("\"} 1\n"));
} }
} }
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) { void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_media_player_state_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_media_player_state_value gauge\n"));
stream->print(F("#TYPE esphome_media_player_volume gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_media_player_volume gauge\n"));
stream->print(F("#TYPE esphome_media_player_is_muted gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_media_player_is_muted gauge\n"));
stream->print(F("#TYPE esphome_media_player_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_media_player_failed gauge\n"));
} }
void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj,
std::string &area, std::string &node, std::string &friendly_name) { std::string &area, std::string &node, std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_) if (obj->is_internal() && !this->include_internal_)
return; return;
stream->print(F("esphome_media_player_failed{id=\"")); stream->print(ESPHOME_F("esphome_media_player_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_media_player_state_value{id=\"")); stream->print(ESPHOME_F("esphome_media_player_state_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",value=\"")); stream->print(ESPHOME_F("\",value=\""));
stream->print(media_player::media_player_state_to_string(obj->state)); stream->print(media_player::media_player_state_to_string(obj->state));
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
stream->print(F("esphome_media_player_volume{id=\"")); stream->print(ESPHOME_F("esphome_media_player_volume{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->volume); stream->print(obj->volume);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
stream->print(F("esphome_media_player_is_muted{id=\"")); stream->print(ESPHOME_F("esphome_media_player_is_muted{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
if (obj->is_muted()) { if (obj->is_muted()) {
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
} else { } else {
stream->print(F("0.0")); stream->print(ESPHOME_F("0.0"));
} }
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void PrometheusHandler::update_entity_type_(AsyncResponseStream *stream) { void PrometheusHandler::update_entity_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_update_entity_state gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_update_entity_state gauge\n"));
stream->print(F("#TYPE esphome_update_entity_info gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_update_entity_info gauge\n"));
stream->print(F("#TYPE esphome_update_entity_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_update_entity_failed gauge\n"));
} }
void PrometheusHandler::handle_update_state_(AsyncResponseStream *stream, update::UpdateState state) { void PrometheusHandler::handle_update_state_(AsyncResponseStream *stream, update::UpdateState state) {
@@ -730,168 +730,168 @@ void PrometheusHandler::update_entity_row_(AsyncResponseStream *stream, update::
return; return;
if (obj->has_state()) { if (obj->has_state()) {
// We have a valid value, output this value // We have a valid value, output this value
stream->print(F("esphome_update_entity_failed{id=\"")); stream->print(ESPHOME_F("esphome_update_entity_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// First update state // First update state
stream->print(F("esphome_update_entity_state{id=\"")); stream->print(ESPHOME_F("esphome_update_entity_state{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",value=\"")); stream->print(ESPHOME_F("\",value=\""));
handle_update_state_(stream, obj->state); handle_update_state_(stream, obj->state);
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
// Next update info // Next update info
stream->print(F("esphome_update_entity_info{id=\"")); stream->print(ESPHOME_F("esphome_update_entity_info{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",current_version=\"")); stream->print(ESPHOME_F("\",current_version=\""));
stream->print(obj->update_info.current_version.c_str()); stream->print(obj->update_info.current_version.c_str());
stream->print(F("\",latest_version=\"")); stream->print(ESPHOME_F("\",latest_version=\""));
stream->print(obj->update_info.latest_version.c_str()); stream->print(obj->update_info.latest_version.c_str());
stream->print(F("\",title=\"")); stream->print(ESPHOME_F("\",title=\""));
stream->print(obj->update_info.title.c_str()); stream->print(obj->update_info.title.c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} else { } else {
// Invalid state // Invalid state
stream->print(F("esphome_update_entity_failed{id=\"")); stream->print(ESPHOME_F("esphome_update_entity_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n")); stream->print(ESPHOME_F("\"} 1\n"));
} }
} }
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
void PrometheusHandler::valve_type_(AsyncResponseStream *stream) { void PrometheusHandler::valve_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_valve_operation gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_valve_operation gauge\n"));
stream->print(F("#TYPE esphome_valve_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_valve_failed gauge\n"));
stream->print(F("#TYPE esphome_valve_position gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_valve_position gauge\n"));
} }
void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node, void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node,
std::string &friendly_name) { std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_) if (obj->is_internal() && !this->include_internal_)
return; return;
stream->print(F("esphome_valve_failed{id=\"")); stream->print(ESPHOME_F("esphome_valve_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n")); stream->print(ESPHOME_F("\"} 0\n"));
// Data itself // Data itself
stream->print(F("esphome_valve_operation{id=\"")); stream->print(ESPHOME_F("esphome_valve_operation{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",operation=\"")); stream->print(ESPHOME_F("\",operation=\""));
stream->print(valve::valve_operation_to_str(obj->current_operation)); stream->print(valve::valve_operation_to_str(obj->current_operation));
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
// Now see if position is supported // Now see if position is supported
if (obj->get_traits().get_supports_position()) { if (obj->get_traits().get_supports_position()) {
stream->print(F("esphome_valve_position{id=\"")); stream->print(ESPHOME_F("esphome_valve_position{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(obj->position); stream->print(obj->position);
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
} }
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void PrometheusHandler::climate_type_(AsyncResponseStream *stream) { void PrometheusHandler::climate_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_climate_setting gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_climate_setting gauge\n"));
stream->print(F("#TYPE esphome_climate_value gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_climate_value gauge\n"));
stream->print(F("#TYPE esphome_climate_failed gauge\n")); stream->print(ESPHOME_F("#TYPE esphome_climate_failed gauge\n"));
} }
void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area,
std::string &node, std::string &friendly_name, std::string &setting, std::string &node, std::string &friendly_name, std::string &setting,
const LogString *setting_value) { const LogString *setting_value) {
stream->print(F("esphome_climate_setting{id=\"")); stream->print(ESPHOME_F("esphome_climate_setting{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",category=\"")); stream->print(ESPHOME_F("\",category=\""));
stream->print(setting.c_str()); stream->print(setting.c_str());
stream->print(F("\",setting_value=\"")); stream->print(ESPHOME_F("\",setting_value=\""));
stream->print(LOG_STR_ARG(setting_value)); stream->print(LOG_STR_ARG(setting_value));
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area,
std::string &node, std::string &friendly_name, std::string &category, std::string &node, std::string &friendly_name, std::string &category,
std::string &climate_value) { std::string &climate_value) {
stream->print(F("esphome_climate_value{id=\"")); stream->print(ESPHOME_F("esphome_climate_value{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",category=\"")); stream->print(ESPHOME_F("\",category=\""));
stream->print(category.c_str()); stream->print(category.c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
stream->print(climate_value.c_str()); stream->print(climate_value.c_str());
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
void PrometheusHandler::climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, void PrometheusHandler::climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area,
std::string &node, std::string &friendly_name, std::string &category, std::string &node, std::string &friendly_name, std::string &category,
bool is_failed_value) { bool is_failed_value) {
stream->print(F("esphome_climate_failed{id=\"")); stream->print(ESPHOME_F("esphome_climate_failed{id=\""));
stream->print(relabel_id_(obj).c_str()); stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area); add_area_label_(stream, area);
add_node_label_(stream, node); add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name); add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\"")); stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str()); stream->print(relabel_name_(obj).c_str());
stream->print(F("\",category=\"")); stream->print(ESPHOME_F("\",category=\""));
stream->print(category.c_str()); stream->print(category.c_str());
stream->print(F("\"} ")); stream->print(ESPHOME_F("\"} "));
if (is_failed_value) { if (is_failed_value) {
stream->print(F("1.0")); stream->print(ESPHOME_F("1.0"));
} else { } else {
stream->print(F("0.0")); stream->print(ESPHOME_F("0.0"));
} }
stream->print(F("\n")); stream->print(ESPHOME_F("\n"));
} }
void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area,

View File

@@ -62,6 +62,11 @@ SPIRAM_SPEEDS = {
} }
def supported() -> bool:
variant = get_esp32_variant()
return variant in SPIRAM_MODES
def validate_psram_mode(config): def validate_psram_mode(config):
esp32_config = fv.full_config.get()[PLATFORM_ESP32] esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if config[CONF_SPEED] == "120MHZ": if config[CONF_SPEED] == "120MHZ":
@@ -95,7 +100,7 @@ def get_config_schema(config):
variant = get_esp32_variant() variant = get_esp32_variant()
speeds = [f"{s}MHZ" for s in SPIRAM_SPEEDS.get(variant, [])] speeds = [f"{s}MHZ" for s in SPIRAM_SPEEDS.get(variant, [])]
if not speeds: if not speeds:
return cv.Invalid("PSRAM is not supported on this chip") raise cv.Invalid("PSRAM is not supported on this chip")
modes = SPIRAM_MODES[variant] modes = SPIRAM_MODES[variant]
return cv.Schema( return cv.Schema(
{ {

View File

@@ -40,7 +40,13 @@ void RemoteTransmitterComponent::await_target_time_() {
if (this->target_time_ == 0) { if (this->target_time_ == 0) {
this->target_time_ = current_time; this->target_time_ = current_time;
} else if ((int32_t) (this->target_time_ - current_time) > 0) { } else if ((int32_t) (this->target_time_ - current_time) > 0) {
#if defined(USE_LIBRETINY)
// busy loop for libretiny is required (see the comment inside micros() in wiring.c)
while ((int32_t) (this->target_time_ - micros()) > 0)
;
#else
delayMicroseconds(this->target_time_ - current_time); delayMicroseconds(this->target_time_ - current_time);
#endif
} }
} }

View File

@@ -374,7 +374,7 @@ void Rtttl::loop() {
this->last_note_ = millis(); this->last_note_ = millis();
} }
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
static const LogString *state_to_string(State state) { static const LogString *state_to_string(State state) {
switch (state) { switch (state) {
case STATE_STOPPED: case STATE_STOPPED:

View File

@@ -124,7 +124,7 @@ async def to_code(config):
template, func_args = parameters_to_template(conf[CONF_PARAMETERS]) template, func_args = parameters_to_template(conf[CONF_PARAMETERS])
trigger = cg.new_Pvariable(conf[CONF_ID], template) trigger = cg.new_Pvariable(conf[CONF_ID], template)
# Add a human-readable name to the script # Add a human-readable name to the script
cg.add(trigger.set_name(conf[CONF_ID].id)) cg.add(trigger.set_name(cg.LogStringLiteral(conf[CONF_ID].id)))
if CONF_MAX_RUNS in conf: if CONF_MAX_RUNS in conf:
cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS])) cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS]))

View File

@@ -48,14 +48,14 @@ template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts..
} }
// Internal function to give scripts readable names. // Internal function to give scripts readable names.
void set_name(const std::string &name) { name_ = name; } void set_name(const LogString *name) { name_ = name; }
protected: protected:
template<int... S> void execute_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) { template<int... S> void execute_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->execute(std::get<S>(tuple)...); this->execute(std::get<S>(tuple)...);
} }
std::string name_; const LogString *name_{nullptr};
}; };
/** A script type for which only a single instance at a time is allowed. /** A script type for which only a single instance at a time is allowed.
@@ -68,7 +68,7 @@ template<typename... Ts> class SingleScript : public Script<Ts...> {
void execute(Ts... x) override { void execute(Ts... x) override {
if (this->is_action_running()) { if (this->is_action_running()) {
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"), this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"),
this->name_.c_str()); LOG_STR_ARG(this->name_));
return; return;
} }
@@ -85,7 +85,7 @@ template<typename... Ts> class RestartScript : public Script<Ts...> {
public: public:
void execute(Ts... x) override { void execute(Ts... x) override {
if (this->is_action_running()) { if (this->is_action_running()) {
this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), this->name_.c_str()); this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), LOG_STR_ARG(this->name_));
this->stop_action(); this->stop_action();
} }
@@ -105,12 +105,12 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
// num_runs_ + 1 // num_runs_ + 1
if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) {
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of queued runs exceeded!"), this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of queued runs exceeded!"),
this->name_.c_str()); LOG_STR_ARG(this->name_));
return; return;
} }
this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"), this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"),
this->name_.c_str()); LOG_STR_ARG(this->name_));
this->num_runs_++; this->num_runs_++;
this->var_queue_.push(std::make_tuple(x...)); this->var_queue_.push(std::make_tuple(x...));
return; return;
@@ -157,7 +157,7 @@ template<typename... Ts> class ParallelScript : public Script<Ts...> {
void execute(Ts... x) override { void execute(Ts... x) override {
if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) {
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"), this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"),
this->name_.c_str()); LOG_STR_ARG(this->name_));
return; return;
} }
this->trigger(x...); this->trigger(x...);

View File

@@ -10,6 +10,39 @@ namespace esphome::sha256 {
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
//
// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains
// internal state that the DMA engine references. This imposes two critical constraints:
//
// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to
// write to incorrect memory locations. This results in null pointer dereferences and crashes.
// ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]).
//
// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same
// function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack
// frame changes (function call/return), the DMA references become invalid and will produce
// truncated hash output (20 bytes instead of 32) or corrupt memory.
//
// CORRECT USAGE:
// void my_function() {
// sha256::SHA256 hasher; // Created locally
// hasher.init();
// hasher.add(data, len); // Any size, no chunking needed
// hasher.calculate();
// bool ok = hasher.equals_hex(expected);
// // hasher destroyed when function returns
// }
//
// INCORRECT USAGE (WILL FAIL ON ESP32-S3):
// void my_function() {
// sha256::SHA256 hasher;
// helper(&hasher); // WRONG: Passed to different stack frame
// }
// void helper(HashBase *h) {
// h->init(); // WRONG: Will produce truncated/corrupted output
// }
SHA256::~SHA256() { mbedtls_sha256_free(&this->ctx_); } SHA256::~SHA256() { mbedtls_sha256_free(&this->ctx_); }
void SHA256::init() { void SHA256::init() {

View File

@@ -39,6 +39,10 @@ class SHA256 : public esphome::HashBase {
protected: protected:
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
// CRITICAL: The mbedtls context MUST be stack-allocated (not a pointer) for ESP32-S3 hardware SHA acceleration.
// The ESP32-S3 DMA engine references this structure's memory addresses. If the context is passed to another
// function (crossing stack frames) or if VLAs are present, the DMA operations will corrupt memory and produce
// truncated/incorrect hash results.
mbedtls_sha256_context ctx_{}; mbedtls_sha256_context ctx_{};
#elif defined(USE_ESP8266) || defined(USE_RP2040) #elif defined(USE_ESP8266) || defined(USE_RP2040)
br_sha256_context ctx_{}; br_sha256_context ctx_{};

View File

@@ -9,7 +9,7 @@
#include "lwip/tcp.h" #include "lwip/tcp.h"
#include <cerrno> #include <cerrno>
#include <cstring> #include <cstring>
#include <queue> #include <array>
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -50,12 +50,18 @@ class LWIPRawImpl : public Socket {
errno = EBADF; errno = EBADF;
return nullptr; return nullptr;
} }
if (accepted_sockets_.empty()) { if (this->accepted_socket_count_ == 0) {
errno = EWOULDBLOCK; errno = EWOULDBLOCK;
return nullptr; return nullptr;
} }
std::unique_ptr<LWIPRawImpl> sock = std::move(accepted_sockets_.front()); // Take from front for FIFO ordering
accepted_sockets_.pop(); std::unique_ptr<LWIPRawImpl> sock = std::move(this->accepted_sockets_[0]);
// Shift remaining sockets forward
for (uint8_t i = 1; i < this->accepted_socket_count_; i++) {
this->accepted_sockets_[i - 1] = std::move(this->accepted_sockets_[i]);
}
this->accepted_socket_count_--;
LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_);
if (addr != nullptr) { if (addr != nullptr) {
sock->getpeername(addr, addrlen); sock->getpeername(addr, addrlen);
} }
@@ -494,9 +500,18 @@ class LWIPRawImpl : public Socket {
// nothing to do here, we just don't push it to the queue // nothing to do here, we just don't push it to the queue
return ERR_OK; return ERR_OK;
} }
// Check if we've reached the maximum accept queue size
if (this->accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) {
LWIP_LOG("Rejecting connection, queue full (%d)", this->accepted_socket_count_);
// Abort the connection when queue is full
tcp_abort(newpcb);
// Must return ERR_ABRT since we called tcp_abort()
return ERR_ABRT;
}
auto sock = make_unique<LWIPRawImpl>(family_, newpcb); auto sock = make_unique<LWIPRawImpl>(family_, newpcb);
sock->init(); sock->init();
accepted_sockets_.push(std::move(sock)); this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock);
LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
return ERR_OK; return ERR_OK;
} }
void err_fn(err_t err) { void err_fn(err_t err) {
@@ -587,7 +602,20 @@ class LWIPRawImpl : public Socket {
} }
struct tcp_pcb *pcb_; struct tcp_pcb *pcb_;
std::queue<std::unique_ptr<LWIPRawImpl>> accepted_sockets_; // Accept queue - holds incoming connections briefly until the event loop calls accept()
// This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop
// 3 slots is plenty since connections are pulled out quickly by the event loop
//
// Memory analysis: std::array<3> vs original std::queue implementation:
// - std::queue uses std::deque internally which on 32-bit systems needs:
// 24 bytes (deque object) + 32+ bytes (map array) + heap allocations
// Total: ~56+ bytes minimum, plus heap fragmentation
// - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes)
// Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations
// Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation)
static constexpr size_t MAX_ACCEPTED_SOCKETS = 3;
std::array<std::unique_ptr<LWIPRawImpl>, MAX_ACCEPTED_SOCKETS> accepted_sockets_;
uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue
bool rx_closed_ = false; bool rx_closed_ = false;
pbuf *rx_buf_ = nullptr; pbuf *rx_buf_ = nullptr;
size_t rx_buf_offset_ = 0; size_t rx_buf_offset_ = 0;

View File

@@ -50,7 +50,7 @@ static const char *const TAG = "sonoff_d1";
uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) { uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) {
uint8_t crc = 0; uint8_t crc = 0;
for (int i = 2; i < len - 1; i++) { for (size_t i = 2; i < len - 1; i++) {
crc += cmd[i]; crc += cmd[i];
} }
return crc; return crc;

View File

@@ -52,17 +52,19 @@ void SPS30Component::setup() {
} else { } else {
result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS);
} }
if (result) {
delay(20);
uint16_t secs[2];
if (this->read_data(secs, 2)) {
this->fan_interval_ = secs[0] << 16 | secs[1];
}
}
this->status_clear_warning(); this->set_timeout(20, [this, result]() {
this->skipped_data_read_cycles_ = 0; if (result) {
this->start_continuous_measurement_(); uint16_t secs[2];
if (this->read_data(secs, 2)) {
this->fan_interval_ = secs[0] << 16 | secs[1];
}
}
this->status_clear_warning();
this->skipped_data_read_cycles_ = 0;
this->start_continuous_measurement_();
this->setup_complete_ = true;
});
}); });
} }
@@ -111,6 +113,8 @@ void SPS30Component::dump_config() {
} }
void SPS30Component::update() { void SPS30Component::update() {
if (!this->setup_complete_)
return;
/// Check if warning flag active (sensor reconnected?) /// Check if warning flag active (sensor reconnected?)
if (this->status_has_warning()) { if (this->status_has_warning()) {
ESP_LOGD(TAG, "Reconnecting"); ESP_LOGD(TAG, "Reconnecting");

View File

@@ -30,9 +30,11 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
bool start_fan_cleaning(); bool start_fan_cleaning();
protected: protected:
bool setup_complete_{false};
uint16_t raw_firmware_version_; uint16_t raw_firmware_version_;
char serial_number_[17] = {0}; /// Terminating NULL character char serial_number_[17] = {0}; /// Terminating NULL character
uint8_t skipped_data_read_cycles_ = 0; uint8_t skipped_data_read_cycles_ = 0;
bool start_continuous_measurement_(); bool start_continuous_measurement_();
enum ErrorCode : uint8_t { enum ErrorCode : uint8_t {

View File

@@ -1,9 +1,10 @@
from ast import literal_eval
import logging import logging
import math import math
import re import re
import jinja2 as jinja import jinja2 as jinja
from jinja2.nativetypes import NativeEnvironment from jinja2.sandbox import SandboxedEnvironment
TemplateError = jinja.TemplateError TemplateError = jinja.TemplateError
TemplateSyntaxError = jinja.TemplateSyntaxError TemplateSyntaxError = jinja.TemplateSyntaxError
@@ -70,7 +71,7 @@ class Jinja:
""" """
def __init__(self, context_vars): def __init__(self, context_vars):
self.env = NativeEnvironment( self.env = SandboxedEnvironment(
trim_blocks=True, trim_blocks=True,
lstrip_blocks=True, lstrip_blocks=True,
block_start_string="<%", block_start_string="<%",
@@ -90,6 +91,15 @@ class Jinja:
**SAFE_GLOBAL_FUNCTIONS, **SAFE_GLOBAL_FUNCTIONS,
} }
def safe_eval(self, expr):
try:
result = literal_eval(expr)
if not isinstance(result, str):
return result
except (ValueError, SyntaxError, MemoryError, TypeError):
pass
return expr
def expand(self, content_str): def expand(self, content_str):
""" """
Renders a string that may contain Jinja expressions or statements Renders a string that may contain Jinja expressions or statements
@@ -106,7 +116,7 @@ class Jinja:
override_vars = content_str.upvalues override_vars = content_str.upvalues
try: try:
template = self.env.from_string(content_str) template = self.env.from_string(content_str)
result = template.render(override_vars) result = self.safe_eval(template.render(override_vars))
if isinstance(result, Undefined): if isinstance(result, Undefined):
# This happens when the expression is simply an undefined variable. Jinja does not # This happens when the expression is simply an undefined variable. Jinja does not
# raise an exception, instead we get "Undefined". # raise an exception, instead we get "Undefined".

View File

@@ -15,6 +15,10 @@ CONF_BANDWIDTH = "bandwidth"
CONF_BITRATE = "bitrate" CONF_BITRATE = "bitrate"
CONF_CODING_RATE = "coding_rate" CONF_CODING_RATE = "coding_rate"
CONF_CRC_ENABLE = "crc_enable" CONF_CRC_ENABLE = "crc_enable"
CONF_CRC_INVERTED = "crc_inverted"
CONF_CRC_SIZE = "crc_size"
CONF_CRC_POLYNOMIAL = "crc_polynomial"
CONF_CRC_INITIAL = "crc_initial"
CONF_DEVIATION = "deviation" CONF_DEVIATION = "deviation"
CONF_DIO1_PIN = "dio1_pin" CONF_DIO1_PIN = "dio1_pin"
CONF_HW_VERSION = "hw_version" CONF_HW_VERSION = "hw_version"
@@ -188,6 +192,14 @@ CONFIG_SCHEMA = (
cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_CRC_INVERTED, default=True): cv.boolean,
cv.Optional(CONF_CRC_SIZE, default=2): cv.int_range(min=1, max=2),
cv.Optional(CONF_CRC_POLYNOMIAL, default=0x1021): cv.All(
cv.hex_int, cv.Range(min=0, max=0xFFFF)
),
cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All(
cv.hex_int, cv.Range(min=0, max=0xFFFF)
),
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
@@ -251,6 +263,10 @@ async def to_code(config):
cg.add(var.set_shaping(config[CONF_SHAPING])) cg.add(var.set_shaping(config[CONF_SHAPING]))
cg.add(var.set_bitrate(config[CONF_BITRATE])) cg.add(var.set_bitrate(config[CONF_BITRATE]))
cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE])) cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE]))
cg.add(var.set_crc_inverted(config[CONF_CRC_INVERTED]))
cg.add(var.set_crc_size(config[CONF_CRC_SIZE]))
cg.add(var.set_crc_polynomial(config[CONF_CRC_POLYNOMIAL]))
cg.add(var.set_crc_initial(config[CONF_CRC_INITIAL]))
cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH])) cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH]))
cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE])) cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE]))
cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT])) cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT]))

View File

@@ -235,6 +235,16 @@ void SX126x::configure() {
buf[7] = (fdev >> 0) & 0xFF; buf[7] = (fdev >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8); this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
// set crc params
if (this->crc_enable_) {
buf[0] = this->crc_initial_ >> 8;
buf[1] = this->crc_initial_ & 0xFF;
this->write_register_(REG_CRC_INITIAL, buf, 2);
buf[0] = this->crc_polynomial_ >> 8;
buf[1] = this->crc_polynomial_ & 0xFF;
this->write_register_(REG_CRC_POLYNOMIAL, buf, 2);
}
// set packet params and sync word // set packet params and sync word
this->set_packet_params_(this->get_max_packet_size()); this->set_packet_params_(this->get_max_packet_size());
if (!this->sync_value_.empty()) { if (!this->sync_value_.empty()) {
@@ -276,7 +286,11 @@ void SX126x::set_packet_params_(uint8_t payload_length) {
buf[4] = 0x00; buf[4] = 0x00;
buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01; buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01;
buf[6] = payload_length; buf[6] = payload_length;
buf[7] = this->crc_enable_ ? 0x06 : 0x01; if (this->crc_enable_) {
buf[7] = (this->crc_inverted_ ? 0x04 : 0x00) + (this->crc_size_ & 0x02);
} else {
buf[7] = 0x01;
}
buf[8] = 0x00; buf[8] = 0x00;
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9); this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9);
} }

View File

@@ -67,6 +67,10 @@ class SX126x : public Component,
void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; } void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; } void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; }
void set_crc_size(uint8_t crc_size) { this->crc_size_ = crc_size; }
void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; }
void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; }
void set_deviation(uint32_t deviation) { this->deviation_ = deviation; } void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
@@ -118,6 +122,11 @@ class SX126x : public Component,
char version_[16]; char version_[16];
SX126xBw bandwidth_{SX126X_BW_125000}; SX126xBw bandwidth_{SX126X_BW_125000};
uint32_t bitrate_{0}; uint32_t bitrate_{0};
bool crc_enable_{false};
bool crc_inverted_{false};
uint8_t crc_size_{0};
uint16_t crc_polynomial_{0};
uint16_t crc_initial_{0};
uint32_t deviation_{0}; uint32_t deviation_{0};
uint32_t frequency_{0}; uint32_t frequency_{0};
uint32_t payload_length_{0}; uint32_t payload_length_{0};
@@ -131,7 +140,6 @@ class SX126x : public Component,
uint8_t shaping_{0}; uint8_t shaping_{0};
uint8_t spreading_factor_{0}; uint8_t spreading_factor_{0};
int8_t pa_power_{0}; int8_t pa_power_{0};
bool crc_enable_{false};
bool rx_start_{false}; bool rx_start_{false};
bool rf_switch_{false}; bool rf_switch_{false};
}; };

View File

@@ -53,6 +53,8 @@ enum SX126xOpCode : uint8_t {
enum SX126xRegister : uint16_t { enum SX126xRegister : uint16_t {
REG_VERSION_STRING = 0x0320, REG_VERSION_STRING = 0x0320,
REG_CRC_INITIAL = 0x06BC,
REG_CRC_POLYNOMIAL = 0x06BE,
REG_GFSK_SYNCWORD = 0x06C0, REG_GFSK_SYNCWORD = 0x06C0,
REG_LORA_SYNCWORD = 0x0740, REG_LORA_SYNCWORD = 0x0740,
REG_OCP = 0x08E7, REG_OCP = 0x08E7,

View File

@@ -50,7 +50,7 @@ void TuyaSelect::dump_config() {
" Options are:", " Options are:",
this->select_id_, this->is_int_ ? "int" : "enum"); this->select_id_, this->is_int_ ? "int" : "enum");
auto options = this->traits.get_options(); auto options = this->traits.get_options();
for (auto i = 0; i < this->mappings_.size(); i++) { for (size_t i = 0; i < this->mappings_.size(); i++) {
ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str());
} }
} }

View File

@@ -1,5 +1,6 @@
#include "valve.h" #include "valve.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <strings.h>
namespace esphome { namespace esphome {
namespace valve { namespace valve {

View File

@@ -242,7 +242,6 @@ void VoiceAssistant::loop() {
msg.flags = flags; msg.flags = flags;
msg.audio_settings = audio_settings; msg.audio_settings = audio_settings;
msg.set_wake_word_phrase(StringRef(this->wake_word_)); msg.set_wake_word_phrase(StringRef(this->wake_word_));
this->wake_word_ = "";
// Reset media player state tracking // Reset media player state tracking
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
@@ -430,8 +429,9 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr
if (this->api_client_ != nullptr) { if (this->api_client_ != nullptr) {
ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant"); ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant");
ESP_LOGE(TAG, "Current client: %s", this->api_client_->get_client_combined_info().c_str()); ESP_LOGE(TAG, "Current client: %s (%s)", this->api_client_->get_name().c_str(),
ESP_LOGE(TAG, "New client: %s", client->get_client_combined_info().c_str()); this->api_client_->get_peername().c_str());
ESP_LOGE(TAG, "New client: %s (%s)", client->get_name().c_str(), client->get_peername().c_str());
return; return;
} }

View File

@@ -9,13 +9,12 @@
namespace esphome { namespace esphome {
namespace web_server { namespace web_server {
#ifdef USE_ARDUINO #ifdef USE_ESP32
ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {}
#elif USE_ARDUINO
ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es) ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es)
: web_server_(ws), events_(es) {} : web_server_(ws), events_(es) {}
#endif #endif
#ifdef USE_ESP_IDF
ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {}
#endif
ListEntitiesIterator::~ListEntitiesIterator() {} ListEntitiesIterator::~ListEntitiesIterator() {}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR

View File

@@ -5,25 +5,24 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/component_iterator.h" #include "esphome/core/component_iterator.h"
namespace esphome { namespace esphome {
#ifdef USE_ESP_IDF #ifdef USE_ESP32
namespace web_server_idf { namespace web_server_idf {
class AsyncEventSource; class AsyncEventSource;
} }
#endif #endif
namespace web_server { namespace web_server {
#ifdef USE_ARDUINO #if !defined(USE_ESP32) && defined(USE_ARDUINO)
class DeferredUpdateEventSource; class DeferredUpdateEventSource;
#endif #endif
class WebServer; class WebServer;
class ListEntitiesIterator : public ComponentIterator { class ListEntitiesIterator : public ComponentIterator {
public: public:
#ifdef USE_ARDUINO #ifdef USE_ESP32
ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es);
#endif
#ifdef USE_ESP_IDF
ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es); ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es);
#elif defined(USE_ARDUINO)
ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es);
#endif #endif
virtual ~ListEntitiesIterator(); virtual ~ListEntitiesIterator();
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@@ -90,11 +89,10 @@ class ListEntitiesIterator : public ComponentIterator {
protected: protected:
const WebServer *web_server_; const WebServer *web_server_;
#ifdef USE_ARDUINO #ifdef USE_ESP32
DeferredUpdateEventSource *events_;
#endif
#ifdef USE_ESP_IDF
esphome::web_server_idf::AsyncEventSource *events_; esphome::web_server_idf::AsyncEventSource *events_;
#elif USE_ARDUINO
DeferredUpdateEventSource *events_;
#endif #endif
}; };

View File

@@ -29,5 +29,5 @@ async def to_code(config):
await ota_to_code(var, config) await ota_to_code(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add_define("USE_WEBSERVER_OTA") cg.add_define("USE_WEBSERVER_OTA")
if CORE.using_esp_idf: if CORE.is_esp32:
add_idf_component(name="zorxx/multipart-parser", ref="1.0.1") add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")

Some files were not shown because too many files have changed in this diff Show More