Compare commits

..

7 Commits

Author SHA1 Message Date
J. Nick Koston
f2682d9df5 response suggestions 2025-10-02 15:22:21 +02:00
Jesse Hills
211a8c872b Add action response to tests 2025-10-01 13:58:19 +13:00
Jesse Hills
f4b7009c96 move callback 2025-10-01 13:50:07 +13:00
Jesse Hills
226399222d move error message 2025-10-01 11:16:07 +13:00
Jesse Hills
9a95ec95f9 Merge branch 'dev' into jesserockz-2025-457 2025-10-01 11:12:55 +13:00
Jesse Hills
2ef4f3c65f Update esphome/components/api/__init__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-01 08:45:58 +13:00
Jesse Hills
6c362d42c3 [api] Add support for getting action responses from home-assistant 2025-09-30 15:28:41 +13:00
95 changed files with 1345 additions and 3206 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -636,13 +636,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -1049,17 +1042,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser(
"upload",

File diff suppressed because it is too large Load Diff

View File

@@ -16,23 +16,26 @@ from esphome.const import (
CONF_KEY,
CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED,
CONF_ON_RESPONSE,
CONF_PASSWORD,
CONF_PORT,
CONF_REBOOT_TIMEOUT,
CONF_RESPONSE_TEMPLATE,
CONF_SERVICE,
CONF_SERVICES,
CONF_TAG,
CONF_TRIGGER_ID,
CONF_VARIABLES,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import TemplateArgsType
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
DOMAIN = "api"
DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"]
AUTO_LOAD = ["socket", "json"]
CODEOWNERS = ["@esphome/core"]
api_ns = cg.esphome_ns.namespace("api")
@@ -40,6 +43,10 @@ APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
HomeAssistantServiceCallAction = api_ns.class_(
"HomeAssistantServiceCallAction", automation.Action
)
ActionResponse = api_ns.class_("ActionResponse")
HomeAssistantActionResponseTrigger = api_ns.class_(
"HomeAssistantActionResponseTrigger", automation.Trigger
)
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
@@ -61,7 +68,6 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_CONNECTIONS = "max_connections"
CONF_MAX_SEND_QUEUE = "max_send_queue"
def validate_encryption_key(value):
@@ -184,19 +190,6 @@ CONFIG_SCHEMA = cv.All(
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),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
@@ -219,7 +212,6 @@ async def to_code(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
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
@@ -288,6 +280,14 @@ async def to_code(config):
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
def _validate_response_config(config):
if CONF_RESPONSE_TEMPLATE in config and not config.get(CONF_ON_RESPONSE):
raise cv.Invalid(
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_ON_RESPONSE}` to be set."
)
return config
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
cv.Schema(
{
@@ -303,10 +303,20 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{cv.string: cv.returning_lambda}
),
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HomeAssistantActionResponseTrigger
),
},
single=True,
),
}
),
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION),
_validate_response_config,
)
@@ -320,7 +330,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
)
async def homeassistant_service_to_code(config, action_id, template_arg, args):
async def homeassistant_service_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
@@ -335,6 +350,24 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args):
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
if response_template := config.get(CONF_RESPONSE_TEMPLATE):
templ = await cg.templatable(response_template, args, cg.std_string)
cg.add(var.set_response_template(templ))
if on_response := config.get(CONF_ON_RESPONSE):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
trigger = cg.new_Pvariable(
on_response[CONF_TRIGGER_ID],
template_arg,
var,
)
await automation.build_automation(
trigger,
[(cg.std_shared_ptr.template(ActionResponse), "response"), *args],
on_response,
)
return var

View File

@@ -780,6 +780,21 @@ message HomeassistantActionRequest {
repeated HomeassistantServiceMap data_template = 3;
repeated HomeassistantServiceMap variables = 4;
bool is_event = 5;
uint32 call_id = 6; // Call ID for response tracking
string response_template = 7 [(no_zero_copy) = true]; // Optional Jinja template for response processing
}
// Message sent by Home Assistant to ESPHome with service call response data
message HomeassistantActionResponse {
option (id) = 130;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
bool success = 2; // Whether the service call succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(pointer_to_buffer) = true]; // Service response data
}
// ==================== IMPORT HOME ASSISTANT STATES ====================

View File

@@ -205,8 +205,7 @@ void APIConnection::loop() {
// Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
on_fatal_error();
ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(),
this->client_info_.peername.c_str());
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
}
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
// Only send ping if we're not disconnecting
@@ -256,7 +255,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
// remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response
// close will happen on next loop
ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str());
this->flags_.next_close = true;
DisconnectResponse resp;
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
@@ -1386,7 +1385,7 @@ void APIConnection::complete_authentication_() {
}
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
#endif
@@ -1550,6 +1549,13 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
}
}
#endif
#if defined(USE_API_HOMEASSISTANT_SERVICES) && defined(USE_API_HOMEASSISTANT_ACTION_RESPONSES)
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message,
reinterpret_cast<const char *>(msg.response_data), msg.response_data_len);
};
#endif
#ifdef USE_API_NOISE
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
NoiseEncryptionSetKeyResponse resp;
@@ -1610,12 +1616,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
#ifdef USE_API_PASSWORD
void APIConnection::on_unauthenticated_access() {
this->on_fatal_error();
ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str());
}
#endif
void APIConnection::on_no_setup_connection() {
this->on_fatal_error();
ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str());
}
void APIConnection::on_fatal_error() {
this->helper_->close();
@@ -1867,8 +1873,8 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_warning_(const LogString *message, APIError err) {
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message),
LOG_STR_ARG(api_error_to_logstr(err)), errno);
}
void APIConnection::log_socket_operation_failed_(APIError err) {

View File

@@ -19,6 +19,14 @@ namespace esphome::api {
struct ClientInfo {
std::string name; // Client name from Hello message
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
@@ -129,6 +137,9 @@ class APIConnection final : public APIServerConnection {
return;
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
#endif
#endif
#ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
@@ -270,8 +281,7 @@ class APIConnection final : public APIServerConnection {
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const std::string &get_name() const { return this->client_info_.name; }
const std::string &get_peername() const { return this->client_info_.peername; }
std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
protected:
// Helper function to handle authentication completion

View File

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

View File

@@ -1,8 +1,7 @@
#pragma once
#include <array>
#include <cstdint>
#include <deque>
#include <limits>
#include <memory>
#include <span>
#include <utility>
#include <vector>
@@ -18,16 +17,6 @@ namespace esphome::api {
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
// Maximum message size limits to prevent OOM on constrained devices
// Voice Assistant is our largest user at 1024 bytes per audio chunk
// Using 2048 + 256 bytes overhead = 2304 bytes total to support voice and future needs
// ESP8266 has very limited RAM and cannot support voice assistant
#ifdef USE_ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 512; // Keep small for memory constrained ESP8266
#else
static constexpr uint16_t MAX_MESSAGE_SIZE = 2304; // Support voice (1024) + headroom for larger messages
#endif
// Forward declaration
struct ClientInfo;
@@ -90,7 +79,7 @@ class APIFrameHelper {
virtual APIError init() = 0;
virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
@@ -172,7 +161,7 @@ class APIFrameHelper {
};
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::deque<SendBuffer> tx_buf_;
std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_;
@@ -185,10 +174,7 @@ class APIFrameHelper {
State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// 8 bytes total, 0 bytes padding
// 5 bytes total, 3 bytes padding
// Common initialization for both plaintext and noise protocols
APIError init_common_();

View File

@@ -24,8 +24,7 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
#define HELPER_LOG(msg, ...) \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
@@ -185,13 +184,6 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
return APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Check against maximum message size to prevent OOM
if (msg_size > MAX_MESSAGE_SIZE) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET;
}
// reserve space for body
if (rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_size);

View File

@@ -18,8 +18,7 @@ namespace esphome::api {
static const char *const TAG = "api.plaintext";
#define HELPER_LOG(msg, ...) \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
@@ -123,10 +122,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
continue;
}
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
MAX_MESSAGE_SIZE);
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();

View File

@@ -884,6 +884,8 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_message(4, it, true);
}
buffer.encode_bool(5, this->is_event);
buffer.encode_uint32(6, this->call_id);
buffer.encode_string(7, this->response_template);
}
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_length(1, this->service_ref_.size());
@@ -891,6 +893,39 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_repeated_message(1, this->data_template);
size.add_repeated_message(1, this->variables);
size.add_bool(1, this->is_event);
size.add_uint32(1, this->call_id);
size.add_length(1, this->response_template.size());
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->call_id = value.as_uint32();
break;
case 2:
this->success = value.as_bool();
break;
default:
return false;
}
return true;
}
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 3:
this->error_message = value.as_string();
break;
case 4: {
// Use raw data directly to avoid allocation
this->response_data = value.data();
this->response_data_len = value.size();
break;
}
default:
return false;
}
return true;
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES

View File

@@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
class HomeassistantActionRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 35;
static constexpr uint8_t ESTIMATED_SIZE = 113;
static constexpr uint8_t ESTIMATED_SIZE = 126;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_request"; }
#endif
@@ -1114,6 +1114,8 @@ class HomeassistantActionRequest final : public ProtoMessage {
std::vector<HomeassistantServiceMap> data_template{};
std::vector<HomeassistantServiceMap> variables{};
bool is_event{false};
uint32_t call_id{0};
std::string response_template{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1123,6 +1125,28 @@ class HomeassistantActionRequest final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
class HomeassistantActionResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 130;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_response"; }
#endif
uint32_t call_id{0};
bool success{false};
std::string error_message{};
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
public:

View File

@@ -1122,6 +1122,19 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
out.append("\n");
}
dump_field(out, "is_event", this->is_event);
dump_field(out, "call_id", this->call_id);
dump_field(out, "response_template", this->response_template);
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void HomeassistantActionResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantActionResponse");
dump_field(out, "call_id", this->call_id);
dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message);
out.append(" response_data: ");
out.append(format_hex_pretty(this->response_data, this->response_data_len));
out.append("\n");
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES

View File

@@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_z_wave_proxy_request(msg);
break;
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
case HomeassistantActionResponse::MESSAGE_TYPE: {
HomeassistantActionResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
#endif
this->on_homeassistant_action_response(msg);
break;
}
#endif
default:
break;

View File

@@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
#endif

View File

@@ -9,12 +9,16 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/version.h"
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include "homeassistant_service.h"
#endif
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
#include <algorithm>
#include <utility>
namespace esphome::api {
@@ -177,8 +181,7 @@ void APIServer::loop() {
// Network is down - disconnect all clients
for (auto &client : this->clients_) {
client->on_fatal_error();
ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
client->client_info_.peername.c_str());
ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
}
// Continue to process and clean up the clients below
}
@@ -400,6 +403,27 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
client->send_homeassistant_action(call);
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
this->action_response_callbacks_[call_id] = std::move(callback);
}
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const char *response_data, size_t response_data_len) {
auto it = this->action_response_callbacks_.find(call_id);
if (it != this->action_response_callbacks_.end()) {
// Create the response object
auto response = std::make_shared<class ActionResponse>(success, error_message, response_data, response_data_len);
// Call the callback
it->second(response);
// Remove the callback as it's one-time use
this->action_response_callbacks_.erase(it);
}
}
#endif
#endif
#ifdef USE_API_HOMEASSISTANT_STATES

View File

@@ -16,6 +16,7 @@
#include "user_services.h"
#endif
#include <map>
#include <vector>
namespace esphome::api {
@@ -110,7 +111,13 @@ class APIServer : public Component, public Controller {
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Action response handling
using ActionResponseCallback = std::function<void(std::shared_ptr<class ActionResponse>)>;
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const char *response_data, size_t response_data_len);
#endif
#endif
#ifdef USE_API_SERVICES
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
@@ -187,6 +194,9 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_SERVICES
std::vector<UserServiceDescriptor *> user_services_;
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
std::map<uint32_t, ActionResponseCallback> action_response_callbacks_;
#endif
// Group smaller types together
uint16_t port_{6053};

View File

@@ -3,8 +3,11 @@
#include "api_server.h"
#ifdef USE_API
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include <functional>
#include <utility>
#include <vector>
#include "api_pb2.h"
#include "esphome/components/json/json_util.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
@@ -44,6 +47,45 @@ template<typename... Ts> class TemplatableKeyValuePair {
TemplatableStringValue<Ts...> value;
};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Represents the response data from a Home Assistant action
class ActionResponse {
public:
ActionResponse(bool success, std::string error_message, const char *data, size_t data_len)
: success_(success), error_message_(std::move(error_message)), data_(data), data_len_(data_len) {}
bool is_success() const { return this->success_; }
const std::string &get_error_message() const { return this->error_message_; }
const char *get_data() const { return this->data_; }
size_t get_data_len() const { return this->data_len_; }
// Get data as parsed JSON object
// Returns unbound JsonObject if data is empty or invalid JSON
JsonObject get_json() {
if (this->data_len_ == 0)
return JsonObject(); // Return unbound JsonObject if no data
if (!this->parsed_json_) {
this->json_document_ = json::parse_json(this->data_, this->data_len_);
this->json_ = this->json_document_.as<JsonObject>();
this->parsed_json_ = true;
}
return this->json_;
}
protected:
bool success_;
std::string error_message_;
const char *data_;
size_t data_len_;
JsonDocument json_document_;
JsonObject json_;
bool parsed_json_{false};
};
// Callback type for action responses
template<typename... Ts> using ActionResponseCallback = std::function<void(std::shared_ptr<ActionResponse>, Ts...)>;
#endif
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
public:
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
@@ -61,6 +103,18 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->variables_.emplace_back(std::move(key), value);
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
this->has_response_template_ = true;
}
void set_response_callback(ActionResponseCallback<Ts...> callback) {
this->wants_response_ = true;
this->response_callback_ = callback;
}
#endif
void play(Ts... x) override {
HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...);
@@ -84,6 +138,27 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
if (this->wants_response_) {
// Generate a unique call ID for this service call
static uint32_t call_id_counter = 1;
uint32_t call_id = call_id_counter++;
resp.call_id = call_id;
// Set response template if provided
if (this->has_response_template_) {
std::string response_template_value = this->response_template_.value(x...);
resp.response_template = response_template_value;
}
auto captured_args = std::make_tuple(x...);
this->parent_->register_action_response_callback(call_id, [this, captured_args](
std::shared_ptr<ActionResponse> response) {
std::apply([this, &response](auto &&...args) { this->response_callback_(response, args...); }, captured_args);
});
}
#endif
this->parent_->send_homeassistant_action(resp);
}
@@ -94,8 +169,25 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
std::vector<TemplatableKeyValuePair<Ts...>> data_;
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
TemplatableStringValue<Ts...> response_template_{""};
ActionResponseCallback<Ts...> response_callback_;
bool wants_response_{false};
bool has_response_template_{false};
#endif
};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename... Ts>
class HomeAssistantActionResponseTrigger : public Trigger<std::shared_ptr<ActionResponse>, Ts...> {
public:
HomeAssistantActionResponseTrigger(HomeAssistantServiceCallAction<Ts...> *action) {
action->set_response_callback(
[this](std::shared_ptr<ActionResponse> response, Ts... x) { this->trigger(response, x...); });
}
};
#endif
} // namespace esphome::api
#endif
#endif

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

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

View File

@@ -73,7 +73,7 @@ void BLECharacteristic::notify() {
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 (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
descriptor->on_write([this](std::span<const uint8_t> value, uint16_t conn_id) {
descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &value, uint16_t conn_id) {
if (value.size() != 2)
return;
uint16_t cccd = encode_uint16(value[1], value[0]);
@@ -208,9 +208,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
if (!param->read.need_rsp)
break; // For some reason you can request a read but not want a response
if (this->on_read_callback_) {
(*this->on_read_callback_)(param->read.conn_id);
}
this->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ,
param->read.conn_id);
uint16_t max_offset = 22;
@@ -278,9 +277,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
}
if (!param->write.is_prep) {
if (this->on_write_callback_) {
(*this->on_write_callback_)(this->value_, param->write.conn_id);
}
this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id);
}
break;
@@ -291,9 +289,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
break;
this->write_event_ = false;
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
if (this->on_write_callback_) {
(*this->on_write_callback_)(this->value_, param->exec_write.conn_id);
}
this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id);
}
esp_err_t err =
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr);

View File

@@ -2,12 +2,10 @@
#include "ble_descriptor.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#include <vector>
#include <span>
#include <functional>
#include <memory>
#ifdef USE_ESP32
@@ -24,10 +22,22 @@ namespace esp32_ble_server {
using namespace esp32_ble;
using namespace bytebuffer;
using namespace event_emitter;
class BLEService;
class BLECharacteristic {
namespace BLECharacteristicEvt {
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:
BLECharacteristic(ESPBTUUID uuid, uint32_t properties);
~BLECharacteristic();
@@ -66,15 +76,6 @@ class BLECharacteristic {
bool is_created();
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:
bool write_event_{false};
BLEService *service_{};
@@ -97,9 +98,6 @@ class BLECharacteristic {
void remove_client_from_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;
enum State : uint8_t {

View File

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

View File

@@ -1,26 +1,30 @@
#pragma once
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32
#include <esp_gatt_defs.h>
#include <esp_gatts_api.h>
#include <span>
#include <functional>
#include <memory>
namespace esphome {
namespace esp32_ble_server {
using namespace esp32_ble;
using namespace bytebuffer;
using namespace event_emitter;
class BLECharacteristic;
// Base class for BLE descriptors
class BLEDescriptor {
namespace BLEDescriptorEvt {
enum VectorEvt {
ON_WRITE,
};
} // namespace BLEDescriptorEvt
class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t> {
public:
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
virtual ~BLEDescriptor();
@@ -35,12 +39,6 @@ class BLEDescriptor {
bool is_created() { return this->state_ == CREATED; }
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:
BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_;
@@ -48,8 +46,6 @@ class BLEDescriptor {
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_{};
enum State : uint8_t {

View File

@@ -147,28 +147,20 @@ BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
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,
esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected");
this->add_client_(param->connect.conn_id);
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id);
break;
}
case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected");
this->remove_client_(param->disconnect.conn_id);
this->parent_->advertising_start();
this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id);
break;
}
case ESP_GATTS_REG_EVT: {

View File

@@ -13,7 +13,6 @@
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#ifdef USE_ESP32
@@ -25,7 +24,18 @@ namespace esp32_ble_server {
using namespace esp32_ble;
using namespace bytebuffer;
class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> {
namespace BLEServerEvt {
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:
void setup() override;
void loop() override;
@@ -55,25 +65,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
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:
enum class CallbackType : uint8_t {
ON_CONNECT,
ON_DISCONNECT,
};
struct CallbackEntry {
CallbackType type;
std::function<void(uint16_t)> callback;
};
struct ServiceEntry {
ESPBTUUID uuid;
uint8_t inst_id;
@@ -84,9 +76,6 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); }
void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); }
void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
std::vector<CallbackEntry> callbacks_;
std::vector<uint8_t> manufacturer_data_{};
esp_gatt_if_t gatts_if_{0};

View File

@@ -14,13 +14,9 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
BLECharacteristic *characteristic) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
BLECharacteristicEvt::VectorEvt::ON_WRITE,
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
return on_write_trigger;
}
#endif
@@ -29,13 +25,9 @@ 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> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
descriptor->on(
BLEDescriptorEvt::VectorEvt::ON_WRITE,
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
return on_write_trigger;
}
#endif
@@ -43,7 +35,8 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
#ifdef USE_ESP32_BLE_SERVER_ON_CONNECT
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)
server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
server->on(BLEServerEvt::EmptyEvt::ON_CONNECT,
[on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
return on_connect_trigger;
}
#endif
@@ -51,22 +44,38 @@ Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *serv
#ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT
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)
server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
[on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
return on_disconnect_trigger;
}
#endif
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic,
EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener) {
// Find and remove existing listener for this characteristic
auto *existing = this->find_listener_(characteristic);
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
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
this->listeners_.push_back({characteristic, pre_notify_listener});
this->listeners_.push_back({characteristic, listener_id, pre_notify_listener_id});
}
BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_(

View File

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

View File

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

View File

@@ -162,7 +162,7 @@ class EthernetComponent : public Component {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern EthernetComponent *global_eth_component;
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
#if defined(USE_ARDUINO) || 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);
#endif

View File

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

View File

@@ -0,0 +1,117 @@
#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,20 +7,24 @@ namespace 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_TEMPERATURE = 0x00;
static const uint8_t HDC1080_CMD_HUMIDITY = 0x01;
void HDC1080Component::setup() {
const uint8_t config[2] = {0x00, 0x00}; // resolution 14bit for both humidity and temperature
const uint8_t data[2] = {
0b00000000, // resolution 14bit for both humidity and temperature
0b00000000 // reserved
};
// if configuration fails - there is a problem
if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
this->mark_failed();
if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) {
// as instruction is same as powerup defaults (for now), interpret as warning if this fails
ESP_LOGW(TAG, "HDC1080 initial config instruction error");
this->status_set_warning();
return;
}
}
void HDC1080Component::dump_config() {
ESP_LOGCONFIG(TAG, "HDC1080:");
LOG_I2C_DEVICE(this);
@@ -31,51 +35,39 @@ void HDC1080Component::dump_config() {
LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_);
}
void HDC1080Component::update() {
// 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();
uint16_t raw_temp;
if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) {
this->status_set_warning();
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);
this->set_timeout(20, [this]() {
uint16_t raw_temperature;
if (this->read(reinterpret_cast<uint8_t *>(&raw_temperature), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
uint16_t raw_humidity;
if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) {
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);
if (this->temperature_ != nullptr) {
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);
}
});
});
ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temp, humidity);
this->status_clear_warning();
}
float HDC1080Component::get_setup_priority() const { return setup_priority::DATA; }
} // namespace hdc1080
} // namespace esphome

View File

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

View File

@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -26,7 +26,7 @@ bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
JsonDocument parse_json(const std::string &data) {
JsonDocument parse_json(const char *data, size_t len) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
#ifdef USE_PSRAM
auto doc_allocator = SpiRamAllocator();
@@ -38,12 +38,12 @@ JsonDocument parse_json(const std::string &data) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return JsonObject(); // return unbound object
}
DeserializationError err = deserializeJson(json_document, data);
DeserializationError err = deserializeJson(json_document, data, len);
if (err == DeserializationError::Ok) {
return json_document;
} else if (err == DeserializationError::NoMemory) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source buffer smaller");
return JsonObject(); // return unbound object
}
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
@@ -51,6 +51,8 @@ JsonDocument parse_json(const std::string &data) {
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
JsonDocument parse_json(const std::string &data) { return parse_json(data.c_str(), data.size()); }
std::string JsonBuilder::serialize() {
if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow");

View File

@@ -51,6 +51,8 @@ std::string build_json(const json_build_t &f);
bool parse_json(const std::string &data, const json_parse_t &f);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
JsonDocument parse_json(const std::string &data);
/// Parse JSON from a buffer and return the root JsonDocument (or an unbound object on error)
JsonDocument parse_json(const char *data, size_t len);
/// Builder class for creating JSON documents without lambdas
class JsonBuilder {

View File

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

View File

@@ -36,31 +36,29 @@ struct device;
namespace esphome::logger {
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
'\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)
// Color and letter constants for log levels
static const char *const LOG_LEVEL_COLORS[] = {
"", // NONE
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE
};
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
'\0', // NONE
'E', // ERROR
'W', // WARNING
'I', // INFO
'C', // CONFIG
'D', // DEBUG
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
static const char *const LOG_LEVEL_LETTERS[] = {
"", // NONE
"E", // ERROR
"W", // WARNING
"I", // INFO
"C", // CONFIG
"D", // DEBUG
"V", // VERBOSE
"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)
/** Enum for logging UART selection
*
@@ -217,6 +215,14 @@ 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
const LogString *get_uart_selection_();
#endif
@@ -312,76 +318,26 @@ class Logger : public Component {
}
#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,
char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
uint16_t pos = *buffer_at;
// Early return if insufficient space - intentionally don't update buffer_at to prevent partial writes
if (pos + MAX_HEADER_SIZE > buffer_size)
return;
// Format header
// uint8_t level is already bounded 0-255, just ensure it's <= 7
if (level > 7)
level = 7;
// Construct: <color>[LEVEL][tag:line]:
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++] = ']';
const char *color = esphome::logger::LOG_LEVEL_COLORS[level];
const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level];
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
if (thread_name != nullptr) {
write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name
buffer[pos++] = '[';
copy_string(buffer, pos, thread_name);
buffer[pos++] = ']';
write_ansi_color_for_level(buffer, pos, level); // Restore original color
// Non-main task with thread name
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line,
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color);
return;
}
#endif
buffer[pos++] = ':';
buffer[pos++] = ' ';
*buffer_at = pos;
// Main task or non ESP32/LibreTiny platform
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line);
}
inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format,

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -17,11 +17,6 @@ from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"]
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")
MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component)
MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord")
@@ -96,20 +91,12 @@ async def to_code(config):
cg.add_define("USE_MDNS")
# Calculate compile-time service count
service_count = sum(
1 for key in COMPONENTS_WITH_MDNS_SERVICES if key in CORE.config
) + len(config[CONF_SERVICES])
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if config[CONF_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]:
txt = [
cg.StructInitializer(

View File

@@ -74,12 +74,32 @@ MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
void MDNSComponent::compile_records_() {
this->hostname_ = App.get_name();
// IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES
// in mdns/__init__.py. If you add a new service here, update both locations.
// Calculate exact capacity needed for services vector
size_t services_count = 0;
#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
if (api::global_api_server != nullptr) {
auto &service = this->services_[this->services_.count()++];
this->services_.emplace_back();
auto &service = this->services_.back();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_port();
@@ -158,23 +178,30 @@ void MDNSComponent::compile_records_() {
#endif // USE_API
#ifdef USE_PROMETHEUS
auto &prom_service = this->services_[this->services_.count()++];
this->services_.emplace_back();
auto &prom_service = this->services_.back();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT;
#endif
#ifdef USE_WEBSERVER
auto &web_service = this->services_[this->services_.count()++];
this->services_.emplace_back();
auto &web_service = this->services_.back();
web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT;
#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)
// 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
auto &fallback_service = this->services_[this->services_.count()++];
this->services_.emplace_back();
auto &fallback_service = this->services_.back();
fallback_service.service_type = "_http";
fallback_service.proto = "_tcp";
fallback_service.port = USE_WEBSERVER_PORT;
@@ -187,7 +214,7 @@ void MDNSComponent::dump_config() {
"mDNS:\n"
" Hostname: %s",
this->hostname_.c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGV(TAG, " Services:");
for (const auto &service : this->services_) {
ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(),
@@ -200,6 +227,8 @@ void MDNSComponent::dump_config() {
#endif
}
std::vector<MDNSService> MDNSComponent::get_services() { return this->services_; }
} // namespace mdns
} // namespace esphome
#endif

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "number_traits.h"
namespace esphome {
@@ -34,10 +33,6 @@ class NumberCall {
NumberCall &with_cycle(bool cycle);
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_;
NumberOperation operation_{NUMBER_OP_NONE};
optional<float> value_;

View File

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

View File

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

View File

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

View File

@@ -40,13 +40,7 @@ void RemoteTransmitterComponent::await_target_time_() {
if (this->target_time_ == 0) {
this->target_time_ = current_time;
} 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);
#endif
}
}

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ class TextTraits {
int get_max_length() const { return this->max_length_; }
// Set/get the pattern.
void set_pattern(const std::string &pattern) { this->pattern_ = pattern; }
void set_pattern(std::string pattern) { this->pattern_ = std::move(pattern); }
std::string get_pattern() const { return this->pattern_; }
StringRef get_pattern_ref() const { return StringRef(this->pattern_); }

View File

@@ -429,9 +429,8 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr
if (this->api_client_ != nullptr) {
ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant");
ESP_LOGE(TAG, "Current client: %s (%s)", this->api_client_->get_name().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());
ESP_LOGE(TAG, "Current client: %s", this->api_client_->get_client_combined_info().c_str());
ESP_LOGE(TAG, "New client: %s", client->get_client_combined_info().c_str());
return;
}

View File

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

View File

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

View File

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

View File

@@ -17,12 +17,6 @@
#endif
#endif // USE_ARDUINO
#if USE_ESP32
using PlatformString = std::string;
#elif USE_ARDUINO
using PlatformString = String;
#endif
namespace esphome {
namespace web_server {
@@ -32,8 +26,8 @@ class OTARequestHandler : public AsyncWebHandler {
public:
OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {}
void handleRequest(AsyncWebServerRequest *request) override;
void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data,
size_t len, bool final) override;
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override;
bool canHandle(AsyncWebServerRequest *request) const override {
// Check if this is an OTA update request
bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST;
@@ -106,7 +100,7 @@ void OTARequestHandler::ota_init_(const char *filename) {
this->ota_success_ = false;
}
void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index,
void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
uint8_t *data, size_t len, bool final) {
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;

View File

@@ -8,7 +8,7 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
#ifdef USE_ARDUINO
#include "StreamString.h"
#endif
@@ -103,7 +103,7 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain)
return match;
}
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
#ifdef USE_ARDUINO
// helper for allowing only unique entries in the queue
void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
DeferredEvent item(source, message_generator);
@@ -127,10 +127,6 @@ void DeferredUpdateEventSource::process_deferred_queue_() {
deferred_queue_.erase(deferred_queue_.begin());
this->consecutive_send_failures_ = 0; // Reset failure count on successful send
} else {
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_()
// The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs
// fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic,
// also update the ESP-IDF implementation.
this->consecutive_send_failures_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead
@@ -301,7 +297,7 @@ void WebServer::setup() {
}
#endif
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
this->base_->add_handler(&this->events_);
#endif
this->base_->add_handler(this);
@@ -385,14 +381,11 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
#endif
// Helper functions to reduce code size by avoiding macro expansion
static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null
const auto &object_id = obj->get_object_id();
snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str());
root["id"] = id_buf;
static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) {
root["id"] = id;
if (start_config == DETAIL_ALL) {
root["name"] = obj->get_name();
root["icon"] = obj->get_icon_ref();
root["icon"] = obj->get_icon();
root["entity_category"] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
if (is_disabled)
@@ -400,19 +393,17 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
}
}
// Keep as separate function even though only used once: reduces code size by ~48 bytes
// by allowing compiler to share code between template instantiations (bool, float, etc.)
template<typename T>
static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value,
static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value,
JsonDetail start_config) {
set_json_id(root, obj, prefix, start_config);
set_json_id(root, obj, id, start_config);
root["value"] = value;
}
template<typename T>
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state,
const T &value, JsonDetail start_config) {
set_json_value(root, obj, prefix, value, start_config);
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id,
const std::string &state, const T &value, JsonDetail start_config) {
set_json_value(root, obj, id, value, start_config);
root["state"] = state;
}
@@ -451,20 +442,20 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
json::JsonBuilder builder;
JsonObject root = builder.root();
const auto uom_ref = obj->get_unit_of_measurement_ref();
// Build JSON directly inline
std::string state;
if (std::isnan(value)) {
state = "NA";
} else {
state = value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref);
state = value_accuracy_to_string(value, obj->get_accuracy_decimals());
if (!obj->get_unit_of_measurement().empty())
state += " " + obj->get_unit_of_measurement();
}
set_json_icon_state_value(root, obj, "sensor", state, value, start_config);
set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
if (!uom_ref.empty())
root["uom"] = uom_ref;
if (!obj->get_unit_of_measurement().empty())
root["uom"] = obj->get_unit_of_measurement();
}
return builder.serialize();
@@ -503,7 +494,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std:
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config);
set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
@@ -576,7 +567,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config);
set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config);
if (start_config == DETAIL_ALL) {
root["assumed_state"] = obj->assumed_state();
this->add_sorting_info_(root, obj);
@@ -616,7 +607,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config)
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "button", start_config);
set_json_id(root, obj, "button-" + obj->get_object_id(), start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
@@ -656,7 +647,8 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config);
set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value,
start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
@@ -725,7 +717,8 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config);
set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state,
start_config);
const auto traits = obj->get_traits();
if (traits.supports_speed()) {
root["speed_level"] = obj->speed;
@@ -800,7 +793,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "light", start_config);
set_json_id(root, obj, "light-" + obj->get_object_id(), start_config);
root["state"] = obj->remote_values.is_on() ? "ON" : "OFF";
light::LightJSONSchema::dump_json(*obj, root);
@@ -836,28 +829,15 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
}
auto call = obj->make_call();
// Lookup table for cover methods
static const struct {
const char *name;
cover::CoverCall &(cover::CoverCall::*action)();
} METHODS[] = {
{"open", &cover::CoverCall::set_command_open},
{"close", &cover::CoverCall::set_command_close},
{"stop", &cover::CoverCall::set_command_stop},
{"toggle", &cover::CoverCall::set_command_toggle},
};
bool found = false;
for (const auto &method : METHODS) {
if (match.method_equals(method.name)) {
(call.*method.action)();
found = true;
break;
}
}
if (!found && !match.method_equals("set")) {
if (match.method_equals("open")) {
call.set_command_open();
} else if (match.method_equals("close")) {
call.set_command_close();
} else if (match.method_equals("stop")) {
call.set_command_stop();
} else if (match.method_equals("toggle")) {
call.set_command_toggle();
} else if (!match.method_equals("set")) {
request->send(404);
return;
}
@@ -888,8 +868,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
start_config);
set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
obj->position, start_config);
root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
if (obj->get_traits().get_supports_position())
@@ -946,9 +926,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
json::JsonBuilder builder;
JsonObject root = builder.root();
const auto uom_ref = obj->traits.get_unit_of_measurement_ref();
set_json_id(root, obj, "number", start_config);
set_json_id(root, obj, "number-" + obj->get_object_id(), start_config);
if (start_config == DETAIL_ALL) {
root["min_value"] =
value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step()));
@@ -956,8 +934,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step()));
root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step()));
root["mode"] = (int) obj->traits.get_mode();
if (!uom_ref.empty())
root["uom"] = uom_ref;
if (!obj->traits.get_unit_of_measurement().empty())
root["uom"] = obj->traits.get_unit_of_measurement();
this->add_sorting_info_(root, obj);
}
if (std::isnan(value)) {
@@ -965,8 +943,10 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
root["state"] = "NA";
} else {
root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()));
root["state"] =
value_accuracy_with_uom_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref);
std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()));
if (!obj->traits.get_unit_of_measurement().empty())
state += " " + obj->traits.get_unit_of_measurement();
root["state"] = state;
}
return builder.serialize();
@@ -1020,7 +1000,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "date", start_config);
set_json_id(root, obj, "date-" + obj->get_object_id(), start_config);
std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day);
root["value"] = value;
root["state"] = value;
@@ -1078,7 +1058,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "time", start_config);
set_json_id(root, obj, "time-" + obj->get_object_id(), start_config);
std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
root["value"] = value;
root["state"] = value;
@@ -1136,7 +1116,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "datetime", start_config);
set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config);
std::string value =
str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second);
root["value"] = value;
@@ -1191,7 +1171,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "text", start_config);
set_json_id(root, obj, "text-" + obj->get_object_id(), start_config);
root["min_length"] = obj->traits.get_min_length();
root["max_length"] = obj->traits.get_max_length();
root["pattern"] = obj->traits.get_pattern();
@@ -1252,7 +1232,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "select", value, value, start_config);
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
if (start_config == DETAIL_ALL) {
JsonArray opt = root["option"].to<JsonArray>();
for (auto &option : obj->traits.get_options()) {
@@ -1321,7 +1301,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "climate", start_config);
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
const auto traits = obj->get_traits();
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
@@ -1474,7 +1454,8 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config);
set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value,
start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
@@ -1502,28 +1483,15 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
}
auto call = obj->make_call();
// Lookup table for valve methods
static const struct {
const char *name;
valve::ValveCall &(valve::ValveCall::*action)();
} METHODS[] = {
{"open", &valve::ValveCall::set_command_open},
{"close", &valve::ValveCall::set_command_close},
{"stop", &valve::ValveCall::set_command_stop},
{"toggle", &valve::ValveCall::set_command_toggle},
};
bool found = false;
for (const auto &method : METHODS) {
if (match.method_equals(method.name)) {
(call.*method.action)();
found = true;
break;
}
}
if (!found && !match.method_equals("set")) {
if (match.method_equals("open")) {
call.set_command_open();
} else if (match.method_equals("close")) {
call.set_command_close();
} else if (match.method_equals("stop")) {
call.set_command_stop();
} else if (match.method_equals("toggle")) {
call.set_command_toggle();
} else if (!match.method_equals("set")) {
request->send(404);
return;
}
@@ -1552,8 +1520,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
start_config);
set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
obj->position, start_config);
root["current_operation"] = valve::valve_operation_to_str(obj->current_operation);
if (obj->get_traits().get_supports_position())
@@ -1587,28 +1555,17 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
auto call = obj->make_call();
parse_string_param_(request, "code", call, &decltype(call)::set_code);
// Lookup table for alarm control panel methods
static const struct {
const char *name;
alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*action)();
} METHODS[] = {
{"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm},
{"arm_away", &alarm_control_panel::AlarmControlPanelCall::arm_away},
{"arm_home", &alarm_control_panel::AlarmControlPanelCall::arm_home},
{"arm_night", &alarm_control_panel::AlarmControlPanelCall::arm_night},
{"arm_vacation", &alarm_control_panel::AlarmControlPanelCall::arm_vacation},
};
bool found = false;
for (const auto &method : METHODS) {
if (match.method_equals(method.name)) {
(call.*method.action)();
found = true;
break;
}
}
if (!found) {
if (match.method_equals("disarm")) {
call.disarm();
} else if (match.method_equals("arm_away")) {
call.arm_away();
} else if (match.method_equals("arm_home")) {
call.arm_home();
} else if (match.method_equals("arm_night")) {
call.arm_night();
} else if (match.method_equals("arm_vacation")) {
call.arm_vacation();
} else {
request->send(404);
return;
}
@@ -1636,8 +1593,8 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro
JsonObject root = builder.root();
char buf[16];
set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)),
value, start_config);
set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(),
PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
@@ -1682,7 +1639,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "event", start_config);
set_json_id(root, obj, "event-" + obj->get_object_id(), start_config);
if (!event_type.empty()) {
root["event_type"] = event_type;
}
@@ -1691,7 +1648,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
for (auto const &event_type : obj->get_event_types()) {
event_types.add(event_type);
}
root["device_class"] = obj->get_device_class_ref();
root["device_class"] = obj->get_device_class();
this->add_sorting_info_(root, obj);
}
@@ -1754,7 +1711,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "update", start_config);
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
root["value"] = obj->update_info.latest_version;
root["state"] = update_state_to_string(obj->state);
if (start_config == DETAIL_ALL) {
@@ -1774,24 +1731,24 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
const auto &url = request->url();
const auto method = request->method();
// Static URL checks
static const char *const STATIC_URLS[] = {
"/",
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
"/events",
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
"/0.css",
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
"/0.js",
#endif
};
// Simple URL checks
if (url == "/")
return true;
for (const auto &static_url : STATIC_URLS) {
if (url == static_url)
return true;
}
#ifdef USE_ARDUINO
if (url == "/events")
return true;
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
if (url == "/0.css")
return true;
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
if (url == "/0.js")
return true;
#endif
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA))
@@ -1811,87 +1768,92 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
if (!is_get_or_post)
return false;
// Use lookup tables for domain checks
static const char *const GET_ONLY_DOMAINS[] = {
// GET-only components
if (is_get) {
#ifdef USE_SENSOR
"sensor",
if (match.domain_equals("sensor"))
return true;
#endif
#ifdef USE_BINARY_SENSOR
"binary_sensor",
if (match.domain_equals("binary_sensor"))
return true;
#endif
#ifdef USE_TEXT_SENSOR
"text_sensor",
if (match.domain_equals("text_sensor"))
return true;
#endif
#ifdef USE_EVENT
"event",
if (match.domain_equals("event"))
return true;
#endif
};
static const char *const GET_POST_DOMAINS[] = {
#ifdef USE_SWITCH
"switch",
#endif
#ifdef USE_BUTTON
"button",
#endif
#ifdef USE_FAN
"fan",
#endif
#ifdef USE_LIGHT
"light",
#endif
#ifdef USE_COVER
"cover",
#endif
#ifdef USE_NUMBER
"number",
#endif
#ifdef USE_DATETIME_DATE
"date",
#endif
#ifdef USE_DATETIME_TIME
"time",
#endif
#ifdef USE_DATETIME_DATETIME
"datetime",
#endif
#ifdef USE_TEXT
"text",
#endif
#ifdef USE_SELECT
"select",
#endif
#ifdef USE_CLIMATE
"climate",
#endif
#ifdef USE_LOCK
"lock",
#endif
#ifdef USE_VALVE
"valve",
#endif
#ifdef USE_ALARM_CONTROL_PANEL
"alarm_control_panel",
#endif
#ifdef USE_UPDATE
"update",
#endif
};
// Check GET-only domains
if (is_get) {
for (const auto &domain : GET_ONLY_DOMAINS) {
if (match.domain_equals(domain))
return true;
}
}
// Check GET+POST domains
// GET+POST components
if (is_get_or_post) {
for (const auto &domain : GET_POST_DOMAINS) {
if (match.domain_equals(domain))
return true;
}
#ifdef USE_SWITCH
if (match.domain_equals("switch"))
return true;
#endif
#ifdef USE_BUTTON
if (match.domain_equals("button"))
return true;
#endif
#ifdef USE_FAN
if (match.domain_equals("fan"))
return true;
#endif
#ifdef USE_LIGHT
if (match.domain_equals("light"))
return true;
#endif
#ifdef USE_COVER
if (match.domain_equals("cover"))
return true;
#endif
#ifdef USE_NUMBER
if (match.domain_equals("number"))
return true;
#endif
#ifdef USE_DATETIME_DATE
if (match.domain_equals("date"))
return true;
#endif
#ifdef USE_DATETIME_TIME
if (match.domain_equals("time"))
return true;
#endif
#ifdef USE_DATETIME_DATETIME
if (match.domain_equals("datetime"))
return true;
#endif
#ifdef USE_TEXT
if (match.domain_equals("text"))
return true;
#endif
#ifdef USE_SELECT
if (match.domain_equals("select"))
return true;
#endif
#ifdef USE_CLIMATE
if (match.domain_equals("climate"))
return true;
#endif
#ifdef USE_LOCK
if (match.domain_equals("lock"))
return true;
#endif
#ifdef USE_VALVE
if (match.domain_equals("valve"))
return true;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
if (match.domain_equals("alarm_control_panel"))
return true;
#endif
#ifdef USE_UPDATE
if (match.domain_equals("update"))
return true;
#endif
}
return false;
@@ -1905,7 +1867,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
return;
}
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
#ifdef USE_ARDUINO
if (url == "/events") {
this->events_.add_new_client(this, request);
return;

View File

@@ -81,7 +81,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround
can be forgotten.
*/
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
#ifdef USE_ARDUINO
using message_generator_t = std::string(WebServer *, void *);
class DeferredUpdateEventSourceList;
@@ -164,7 +164,7 @@ class DeferredUpdateEventSourceList : public std::list<DeferredUpdateEventSource
* can be found under https://esphome.io/web-api/index.html.
*/
class WebServer : public Controller, public Component, public AsyncWebHandler {
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
#ifdef USE_ARDUINO
friend class DeferredUpdateEventSourceList;
#endif
@@ -559,11 +559,12 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
}
web_server_base::WebServerBase *base_;
#ifdef USE_ESP32
AsyncEventSource events_{"/events", this};
#elif USE_ARDUINO
#ifdef USE_ARDUINO
DeferredUpdateEventSourceList events_;
#endif
#ifdef USE_ESP_IDF
AsyncEventSource events_{"/events", this};
#endif
#if USE_WEBSERVER_VERSION == 1
const char *css_url_{nullptr};

View File

@@ -34,23 +34,23 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/html");
const std::string &title = App.get_name();
stream->print(ESPHOME_F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta "
"name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>"));
stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta "
"name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>"));
stream->print(title.c_str());
stream->print(ESPHOME_F("</title>"));
stream->print(F("</title>"));
#ifdef USE_WEBSERVER_CSS_INCLUDE
stream->print(ESPHOME_F("<link rel=\"stylesheet\" href=\"/0.css\">"));
stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">"));
#endif
if (strlen(this->css_url_) > 0) {
stream->print(ESPHOME_F(R"(<link rel="stylesheet" href=")"));
stream->print(F(R"(<link rel="stylesheet" href=")"));
stream->print(this->css_url_);
stream->print(ESPHOME_F("\">"));
stream->print(F("\">"));
}
stream->print(ESPHOME_F("</head><body>"));
stream->print(ESPHOME_F("<article class=\"markdown-body\"><h1>"));
stream->print(F("</head><body>"));
stream->print(F("<article class=\"markdown-body\"><h1>"));
stream->print(title.c_str());
stream->print(ESPHOME_F("</h1>"));
stream->print(ESPHOME_F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>"));
stream->print(F("</h1>"));
stream->print(F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>"));
#ifdef USE_SENSOR
for (auto *obj : App.get_sensors()) {
@@ -190,28 +190,26 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
}
#endif
stream->print(
ESPHOME_F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
// Show OTA form only if web_server OTA is not explicitly disabled
// Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
stream->print(
ESPHOME_F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
stream->print(F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
#endif
stream->print(ESPHOME_F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
#ifdef USE_WEBSERVER_JS_INCLUDE
if (this->js_include_ != nullptr) {
stream->print(ESPHOME_F("<script type=\"module\" src=\"/0.js\"></script>"));
stream->print(F("<script type=\"module\" src=\"/0.js\"></script>"));
}
#endif
if (strlen(this->js_url_) > 0) {
stream->print(ESPHOME_F("<script src=\""));
stream->print(F("<script src=\""));
stream->print(this->js_url_);
stream->print(ESPHOME_F("\"></script>"));
stream->print(F("\"></script>"));
}
stream->print(ESPHOME_F("</article></body></html>"));
stream->print(F("</article></body></html>"));
request->send(stream);
}

View File

@@ -9,10 +9,10 @@ DEPENDENCIES = ["network"]
def AUTO_LOAD():
if CORE.is_esp32:
return ["web_server_idf"]
if CORE.using_arduino:
return ["async_tcp"]
if CORE.using_esp_idf:
return ["web_server_idf"]
return []
@@ -33,9 +33,6 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}"))
if CORE.is_esp32:
return
if CORE.using_arduino:
if CORE.is_esp32:
cg.add_library("WiFi", None)

View File

@@ -7,31 +7,11 @@
#include "esphome/core/component.h"
// Platform-agnostic macros for web server components
// On ESP32 (both Arduino and IDF): Use plain strings (no PROGMEM)
// On ESP8266: Use Arduino's F() macro for PROGMEM strings
#ifdef USE_ESP32
#define ESPHOME_F(string_literal) (string_literal)
#define ESPHOME_PGM_P const char *
#define ESPHOME_strncpy_P strncpy
#else
// ESP8266 uses Arduino macros
#define ESPHOME_F(string_literal) F(string_literal)
#define ESPHOME_PGM_P PGM_P
#define ESPHOME_strncpy_P strncpy_P
#endif
#if USE_ESP32
#ifdef USE_ARDUINO
#include <ESPAsyncWebServer.h>
#elif USE_ESP_IDF
#include "esphome/core/hal.h"
#include "esphome/components/web_server_idf/web_server_idf.h"
#else
#include <ESPAsyncWebServer.h>
#endif
#if USE_ESP32
using PlatformString = std::string;
#elif USE_ARDUINO
using PlatformString = String;
#endif
namespace esphome {
@@ -48,8 +28,8 @@ class MiddlewareHandler : public AsyncWebHandler {
bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); }
void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); }
void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data,
size_t len, bool final) override {
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override {
next_->handleUpload(request, filename, index, data, len, final);
}
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override {
@@ -85,8 +65,8 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
return;
MiddlewareHandler::handleRequest(request);
}
void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data,
size_t len, bool final) override {
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override {
if (!check_auth(request))
return;
MiddlewareHandler::handleUpload(request, filename, index, data, len, final);
@@ -131,8 +111,8 @@ class WebServerBase : public Component {
float get_setup_priority() const override;
#ifdef USE_WEBSERVER_AUTH
void set_auth_username(const std::string &auth_username) { credentials_.username = auth_username; }
void set_auth_password(const std::string &auth_password) { credentials_.password = auth_password; }
void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); }
void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); }
#endif
void add_handler(AsyncWebHandler *handler);

View File

@@ -5,7 +5,7 @@ CODEOWNERS = ["@dentra"]
CONFIG_SCHEMA = cv.All(
cv.Schema({}),
cv.only_on_esp32,
cv.only_with_esp_idf,
)

View File

@@ -1,5 +1,5 @@
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
#include "multipart.h"
#include "utils.h"
#include "esphome/core/log.h"
@@ -251,4 +251,4 @@ std::string str_trim(const std::string &str) {
} // namespace web_server_idf
} // namespace esphome
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)

View File

@@ -1,6 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
#include <cctype>
#include <cstring>
@@ -35,7 +35,7 @@ class MultipartReader {
// Set callbacks for handling data
void set_data_callback(DataCallback callback) { data_callback_ = std::move(callback); }
void set_part_complete_callback(const PartCompleteCallback &callback) { part_complete_callback_ = callback; }
void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = std::move(callback); }
// Parse incoming data
size_t parse(const char *data, size_t len);
@@ -83,4 +83,4 @@ std::string str_trim(const std::string &str);
} // namespace web_server_idf
} // namespace esphome
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)

View File

@@ -1,4 +1,4 @@
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include <memory>
#include <cstring>
#include <cctype>
@@ -122,4 +122,4 @@ const char *stristr(const char *haystack, const char *needle) {
} // namespace web_server_idf
} // namespace esphome
#endif // USE_ESP32
#endif // USE_ESP_IDF

View File

@@ -1,5 +1,5 @@
#pragma once
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include <esp_http_server.h>
#include <string>
@@ -24,4 +24,4 @@ const char *stristr(const char *haystack, const char *needle);
} // namespace web_server_idf
} // namespace esphome
#endif // USE_ESP32
#endif // USE_ESP_IDF

View File

@@ -1,4 +1,4 @@
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include <cstdarg>
#include <memory>
@@ -25,10 +25,6 @@
#include "esphome/components/web_server/list_entities.h"
#endif // USE_WEBSERVER
// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts
#include <cerrno>
#include <sys/socket.h>
namespace esphome {
namespace web_server_idf {
@@ -50,28 +46,6 @@ DefaultHeaders default_headers_instance;
DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
namespace {
// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
if (buf == nullptr) {
return HTTPD_SOCK_ERR_INVALID;
}
// Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Buffer full - retry later
return HTTPD_SOCK_ERR_TIMEOUT;
}
// Real error
ESP_LOGD(TAG, "send error: errno %d", errno);
return HTTPD_SOCK_ERR_FAIL;
}
return ret;
}
} // namespace
void AsyncWebServer::end() {
if (this->server_) {
httpd_stop(this->server_);
@@ -190,8 +164,8 @@ esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const
AsyncWebServerRequest::~AsyncWebServerRequest() {
delete this->rsp_;
for (auto *param : this->params_) {
delete param; // NOLINT(cppcoreguidelines-owning-memory)
for (const auto &pair : this->params_) {
delete pair.second; // NOLINT(cppcoreguidelines-owning-memory)
}
}
@@ -231,22 +205,10 @@ void AsyncWebServerRequest::redirect(const std::string &url) {
}
void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) {
// Set status code - use constants for common codes to avoid string allocation
const char *status = nullptr;
switch (code) {
case 200:
status = HTTPD_200;
break;
case 404:
status = HTTPD_404;
break;
case 409:
status = HTTPD_409;
break;
default:
break;
}
httpd_resp_set_status(*this, status == nullptr ? to_string(code).c_str() : status);
httpd_resp_set_status(*this, code == 200 ? HTTPD_200
: code == 404 ? HTTPD_404
: code == 409 ? HTTPD_409
: to_string(code).c_str());
if (content_type && *content_type) {
httpd_resp_set_type(*this, content_type);
@@ -303,14 +265,11 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
#endif
AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
// Check cache first - only successful lookups are cached
for (auto *param : this->params_) {
if (param->name() == name) {
return param;
}
auto find = this->params_.find(name);
if (find != this->params_.end()) {
return find->second;
}
// Look up value from query strings
optional<std::string> val = query_key_value(this->post_query_, name);
if (!val.has_value()) {
auto url_query = request_get_url_query(*this);
@@ -319,14 +278,11 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
}
}
// Don't cache misses to prevent memory exhaustion from malicious requests
// with thousands of non-existent parameter lookups
if (!val.has_value()) {
return nullptr;
AsyncWebParameter *param = nullptr;
if (val.has_value()) {
param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory)
}
auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory)
this->params_.push_back(param);
this->params_.insert({name, param});
return param;
}
@@ -428,9 +384,6 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
this->hd_ = req->handle;
this->fd_.store(httpd_req_to_sockfd(req));
// Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
// Configure reconnect timeout and send config
// this should always go through since the tcp send buffer is empty on connect
std::string message = ws->get_config_json();
@@ -506,45 +459,15 @@ void AsyncEventSourceResponse::process_buffer_() {
return;
}
size_t remaining = event_buffer_.size() - event_bytes_sent_;
int bytes_sent =
httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
// EAGAIN/EWOULDBLOCK - socket buffer full, try again later
// NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_()
// The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs
// close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also
// update the Arduino implementation.
this->consecutive_send_failures_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead
ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
this->consecutive_send_failures_);
this->fd_.store(0); // Mark for cleanup
this->deferred_queue_.clear();
}
int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_,
event_buffer_.size() - event_bytes_sent_, 0);
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) {
// Socket error - just return, the connection will be closed by httpd
// and our destroy callback will be called
return;
}
if (bytes_sent == HTTPD_SOCK_ERR_FAIL) {
// Real socket error - connection will be closed by httpd and destroy callback will be called
return;
}
if (bytes_sent <= 0) {
// Unexpected error or zero bytes sent
ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent);
return;
}
// Successful send - reset failure counter
this->consecutive_send_failures_ = 0;
event_bytes_sent_ += bytes_sent;
// Log partial sends for debugging
if (event_bytes_sent_ < event_buffer_.size()) {
ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_,
event_buffer_.size());
}
if (event_bytes_sent_ == event_buffer_.size()) {
event_buffer_.resize(0);
event_bytes_sent_ = 0;
@@ -747,4 +670,4 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
} // namespace web_server_idf
} // namespace esphome
#endif // !defined(USE_ESP32)
#endif // !defined(USE_ESP_IDF)

View File

@@ -1,5 +1,5 @@
#pragma once
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include "esphome/core/defines.h"
#include <esp_http_server.h>
@@ -22,14 +22,18 @@ class ListEntitiesIterator;
#endif
namespace web_server_idf {
#define F(string_literal) (string_literal)
#define PGM_P const char *
#define strncpy_P strncpy
using String = std::string;
class AsyncWebParameter {
public:
AsyncWebParameter(std::string name, std::string value) : name_(std::move(name)), value_(std::move(value)) {}
const std::string &name() const { return this->name_; }
AsyncWebParameter(std::string value) : value_(std::move(value)) {}
const std::string &value() const { return this->value_; }
protected:
std::string name_;
std::string value_;
};
@@ -170,10 +174,7 @@ class AsyncWebServerRequest {
protected:
httpd_req_t *req_;
AsyncWebServerResponse *rsp_{};
// Use vector instead of map/unordered_map: most requests have 0-3 params, so linear search
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
// duplicate storage. Only successful lookups are cached to prevent memory exhaustion attacks.
std::vector<AsyncWebParameter *> params_;
std::map<std::string, AsyncWebParameter *> params_;
std::string post_query_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {}
@@ -282,8 +283,6 @@ class AsyncEventSourceResponse {
std::unique_ptr<esphome::web_server::ListEntitiesIterator> entities_iterator_;
std::string event_buffer_{""};
size_t event_bytes_sent_;
uint16_t consecutive_send_failures_{0};
static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate
};
using AsyncEventSourceClient = AsyncEventSourceResponse;
@@ -342,4 +341,4 @@ class DefaultHeaders {
using namespace esphome::web_server_idf; // NOLINT(google-global-names-in-headers)
#endif // !defined(USE_ESP32)
#endif // !defined(USE_ESP_IDF)

View File

@@ -817,6 +817,7 @@ CONF_RESET_DURATION = "reset_duration"
CONF_RESET_PIN = "reset_pin"
CONF_RESIZE = "resize"
CONF_RESOLUTION = "resolution"
CONF_RESPONSE_TEMPLATE = "response_template"
CONF_RESTART = "restart"
CONF_RESTORE = "restore"
CONF_RESTORE_MODE = "restore_mode"
@@ -1169,7 +1170,7 @@ UNIT_KILOMETER = "km"
UNIT_KILOMETER_PER_HOUR = "km/h"
UNIT_KILOVOLT_AMPS = "kVA"
UNIT_KILOVOLT_AMPS_HOURS = "kVAh"
UNIT_KILOVOLT_AMPS_REACTIVE = "kvar"
UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR"
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh"
UNIT_KILOWATT = "kW"
UNIT_KILOWATT_HOURS = "kWh"

View File

@@ -703,15 +703,6 @@ class EsphomeCore:
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
return self.relative_build_path(".piolibdeps", *path)
@property
def platformio_cache_dir(self) -> str:
"""Get the PlatformIO cache directory path."""
# Check if running in Docker/HA addon with custom cache dir
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
return cache_dir
# Default PlatformIO cache location
return os.path.expanduser("~/.platformio/.cache")
@property
def firmware_bin(self) -> Path:
if self.is_libretiny:

View File

@@ -33,22 +33,12 @@ static const char *const TAG = "component";
// Using namespace-scope static to avoid guard variables (saves 16 bytes total)
// This is safe because ESPHome is single-threaded during initialization
namespace {
struct ComponentErrorMessage {
const Component *component;
const char *message;
};
struct ComponentPriorityOverride {
const Component *component;
float priority;
};
// Error messages for failed components
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> component_error_messages;
// Setup priority overrides - freed after setup completes
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
std::unique_ptr<std::vector<std::pair<const Component *, float>>> setup_priority_overrides;
} // namespace
namespace setup_priority {
@@ -144,9 +134,9 @@ void Component::call_dump_config() {
// Look up error message from global vector
const char *error_msg = nullptr;
if (component_error_messages) {
for (const auto &entry : *component_error_messages) {
if (entry.component == this) {
error_msg = entry.message;
for (const auto &pair : *component_error_messages) {
if (pair.first == this) {
error_msg = pair.second;
break;
}
}
@@ -316,17 +306,17 @@ void Component::status_set_error(const char *message) {
if (message != nullptr) {
// Lazy allocate the error messages vector if needed
if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
component_error_messages = std::make_unique<std::vector<std::pair<const Component *, const char *>>>();
}
// Check if this component already has an error message
for (auto &entry : *component_error_messages) {
if (entry.component == this) {
entry.message = message;
for (auto &pair : *component_error_messages) {
if (pair.first == this) {
pair.second = message;
return;
}
}
// Add new error message
component_error_messages->emplace_back(ComponentErrorMessage{this, message});
component_error_messages->emplace_back(this, message);
}
}
void Component::status_clear_warning() {
@@ -366,9 +356,9 @@ float Component::get_actual_setup_priority() const {
// Check if there's an override in the global vector
if (setup_priority_overrides) {
// Linear search is fine for small n (typically < 5 overrides)
for (const auto &entry : *setup_priority_overrides) {
if (entry.component == this) {
return entry.priority;
for (const auto &pair : *setup_priority_overrides) {
if (pair.first == this) {
return pair.second;
}
}
}
@@ -377,21 +367,21 @@ float Component::get_actual_setup_priority() const {
void Component::set_setup_priority(float priority) {
// Lazy allocate the vector if needed
if (!setup_priority_overrides) {
setup_priority_overrides = std::make_unique<std::vector<ComponentPriorityOverride>>();
setup_priority_overrides = std::make_unique<std::vector<std::pair<const Component *, float>>>();
// Reserve some space to avoid reallocations (most configs have < 10 overrides)
setup_priority_overrides->reserve(10);
}
// Check if this component already has an override
for (auto &entry : *setup_priority_overrides) {
if (entry.component == this) {
entry.priority = priority;
for (auto &pair : *setup_priority_overrides) {
if (pair.first == this) {
pair.second = priority;
return;
}
}
// Add new override
setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority});
setup_priority_overrides->emplace_back(this, priority);
}
bool Component::has_overridden_loop() const {

View File

@@ -168,9 +168,8 @@ class ComponentIterator {
UPDATE,
#endif
MAX,
};
} state_{IteratorState::NONE};
uint16_t at_{0}; // Supports up to 65,535 entities per type
IteratorState state_{IteratorState::NONE};
bool include_internal_{false};
template<typename Container>

View File

@@ -82,7 +82,6 @@
#define USE_LVGL_TILEVIEW
#define USE_LVGL_TOUCHSCREEN
#define USE_MDNS
#define MDNS_SERVICE_COUNT 3
#define USE_MEDIA_PLAYER
#define USE_NEXTION_TFT_UPLOAD
#define USE_NUMBER
@@ -112,11 +111,11 @@
#define USE_API_CLIENT_CONNECTED_TRIGGER
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
#define USE_API_HOMEASSISTANT_SERVICES
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES
#define USE_API_HOMEASSISTANT_STATES
#define USE_API_NOISE
#define USE_API_PLAINTEXT
#define USE_API_SERVICES
#define API_MAX_SEND_QUEUE 8
#define USE_MD5
#define USE_SHA256
#define USE_MQTT

View File

@@ -3,7 +3,6 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#include <strings.h>
#include <algorithm>
@@ -349,34 +348,17 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
return PARSE_NONE;
}
static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) {
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
if (accuracy_decimals < 0) {
auto multiplier = powf(10.0f, accuracy_decimals);
value = roundf(value * multiplier) / multiplier;
accuracy_decimals = 0;
}
}
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
normalize_accuracy_decimals(value, accuracy_decimals);
char tmp[32]; // should be enough, but we should maybe improve this at some point.
snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value);
return std::string(tmp);
}
std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) {
normalize_accuracy_decimals(value, accuracy_decimals);
// Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm")
// snprintf truncates safely if exceeded, though ESPHome UOMs are typically short
char tmp[64];
if (unit_of_measurement.empty()) {
snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value);
} else {
snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str());
}
return std::string(tmp);
}
int8_t step_to_accuracy_decimals(float step) {
// use printf %g to find number of digits based on temperature step
char buf[32];
@@ -390,8 +372,10 @@ int8_t step_to_accuracy_decimals(float step) {
return str.length() - dot_pos - 1;
}
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// Helper function to find the index of a base64 character in the lookup table.
// Returns the character's position (0-63) if found, or 0 if not found.
@@ -401,8 +385,8 @@ static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
// stops processing at the first invalid character due to the is_base64() check in its
// while loop condition, making this edge case harmless in practice.
static inline uint8_t base64_find_char(char c) {
const void *ptr = memchr(BASE64_CHARS, c, sizeof(BASE64_CHARS));
return ptr ? (static_cast<const char *>(ptr) - BASE64_CHARS) : 0;
const char *pos = strchr(BASE64_CHARS, c);
return pos ? (pos - BASE64_CHARS) : 0;
}
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }

View File

@@ -45,9 +45,6 @@
namespace esphome {
// Forward declaration to avoid circular dependency with string_ref.h
class StringRef;
/// @name STL backports
///@{
@@ -133,9 +130,6 @@ template<typename T, size_t N> class StaticVector {
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
// Direct access to size counter for efficient in-place construction
size_t &count() { return count_; }
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
@@ -606,8 +600,6 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch
/// Create a string from a value and an accuracy in decimals.
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals);
/// Create a string from a value, an accuracy in decimals, and a unit of measurement.
std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement);
/// Derive accuracy in decimals from an increment step.
int8_t step_to_accuracy_decimals(float step);

View File

@@ -95,9 +95,10 @@ class Scheduler {
} name_;
uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// even when devices run for months. Split into two fields for better memory
// alignment on 32-bit systems.
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)

View File

@@ -192,7 +192,7 @@ def install_custom_components_meta_finder():
install_meta_finder(custom_components_dir)
def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None:
def _lookup_module(domain, exception):
if domain in _COMPONENT_CACHE:
return _COMPONENT_CACHE[domain]
@@ -219,16 +219,16 @@ def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None:
return manif
def get_component(domain: str, exception: bool = False) -> ComponentManifest | None:
def get_component(domain, exception=False):
assert "." not in domain
return _lookup_module(domain, exception)
def get_platform(domain: str, platform: str) -> ComponentManifest | None:
def get_platform(domain, platform):
full = f"{platform}.{domain}"
return _lookup_module(full, False)
_COMPONENT_CACHE: dict[str, ComponentManifest] = {}
_COMPONENT_CACHE = {}
CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve()
_COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config)

View File

@@ -5,7 +5,6 @@ import os
from pathlib import Path
import re
import subprocess
from typing import Any
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
from esphome.core import CORE, EsphomeError
@@ -112,16 +111,7 @@ def run_compile(config, verbose):
args = []
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
result = run_platformio_cli_run(config, verbose, *args)
# Run memory analysis if enabled
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
try:
analyze_memory_usage(config)
except Exception as e:
_LOGGER.warning("Failed to analyze memory usage: %s", e)
return result
return run_platformio_cli_run(config, verbose, *args)
def _run_idedata(config):
@@ -350,93 +340,3 @@ class IDEData:
return f"{self.cc_path[:-7]}addr2line.exe"
return f"{self.cc_path[:-3]}addr2line"
@property
def objdump_path(self) -> str:
# replace gcc at end with objdump
# Windows
if self.cc_path.endswith(".exe"):
return f"{self.cc_path[:-7]}objdump.exe"
return f"{self.cc_path[:-3]}objdump"
@property
def readelf_path(self) -> str:
# replace gcc at end with readelf
# Windows
if self.cc_path.endswith(".exe"):
return f"{self.cc_path[:-7]}readelf.exe"
return f"{self.cc_path[:-3]}readelf"
def analyze_memory_usage(config: dict[str, Any]) -> None:
"""Analyze memory usage by component after compilation."""
# Lazy import to avoid overhead when not needed
from esphome.analyze_memory import MemoryAnalyzer
idedata = get_idedata(config)
# Get paths to tools
elf_path = idedata.firmware_elf_path
objdump_path = idedata.objdump_path
readelf_path = idedata.readelf_path
# Debug logging
_LOGGER.debug("ELF path from idedata: %s", elf_path)
# Check if file exists
if not Path(elf_path).exists():
# Try alternate path
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
if alt_path.exists():
elf_path = str(alt_path)
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
else:
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
return
# Extract external components from config
external_components = set()
# Get the list of built-in ESPHome components
from esphome.analyze_memory import get_esphome_components
builtin_components = get_esphome_components()
# Special non-component keys that appear in configs
NON_COMPONENT_KEYS = {
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"<<",
}
# Check all top-level keys in config
for key in config:
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
# This is an external component
external_components.add(key)
_LOGGER.debug("Detected external components: %s", external_components)
# Create analyzer and run analysis
analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components)
analyzer.analyze()
# Generate and print report
report = analyzer.generate_report()
_LOGGER.info("\n%s", report)
# Optionally save to file
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
if report_file.suffix == ".json":
report_file.write_text(analyzer.to_json())
_LOGGER.info("Memory report saved to %s", report_file)
else:
report_file.write_text(report)
_LOGGER.info("Memory report saved to %s", report_file)

View File

@@ -72,6 +72,7 @@ lib_deps =
SPI ; spi (Arduino built-in)
Wire ; i2c (Arduino built-int)
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
fastled/FastLED@3.9.16 ; fastled_base
freekode/TM1651@1.0.1 ; tm1651
glmnet/Dsmr@0.7 ; dsmr
@@ -106,7 +107,6 @@ lib_deps =
ESP8266WiFi ; wifi (Arduino built-in)
Update ; ota (Arduino built-in)
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
makuna/NeoPixelBus@2.7.3 ; neopixelbus
ESP8266HTTPClient ; http_request (Arduino built-in)
ESP8266mDNS ; mdns (Arduino built-in)
@@ -129,7 +129,7 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/
platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip
framework = arduino, espidf ; Arduino as an ESP-IDF component
framework = arduino
lib_deps =
; order matters with lib-deps; some of the libs in common:arduino.lib_deps
; don't declare built-in libraries as dependencies, so they have to be declared first
@@ -193,7 +193,6 @@ platform_packages =
framework = arduino
lib_deps =
${common:arduino.lib_deps}
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
build_flags =
${common:arduino.build_flags}
-DUSE_RP2040
@@ -208,8 +207,7 @@ platform = libretiny@1.9.1
framework = arduino
lib_compat_mode = soft
lib_deps =
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
droscy/esp_wireguard@0.4.2 ; wireguard
droscy/esp_wireguard@0.4.2 ; wireguard
build_flags =
${common:arduino.build_flags}
-DUSE_LIBRETINY
@@ -276,7 +274,6 @@ build_unflags =
[env:esp32-arduino-tidy]
extends = common:esp32-arduino
board = esp32dev
board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-arduino-tidy
build_flags =
${common:esp32-arduino.build_flags}
${flags:clangtidy.build_flags}

View File

@@ -1,194 +0,0 @@
"""Tests for PSRAM component."""
from typing import Any
import pytest
from esphome.components.esp32.const import (
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
import esphome.config_validation as cv
from esphome.const import CONF_ESPHOME, PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
UNSUPPORTED_PSRAM_VARIANTS = [
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
]
SUPPORTED_PSRAM_VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32P4,
]
@pytest.mark.parametrize(
("config", "error_match"),
[
pytest.param(
{},
r"PSRAM is not supported on this chip",
id="psram_not_supported",
),
],
)
@pytest.mark.parametrize("variant", UNSUPPORTED_PSRAM_VARIANTS)
def test_psram_configuration_errors_unsupported_variants(
config: Any,
error_match: str,
variant: str,
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: variant},
full_config={CONF_ESPHOME: {}},
)
"""Test detection of invalid PSRAM configuration on unsupported variants."""
from esphome.components.psram import CONFIG_SCHEMA
with pytest.raises(cv.Invalid, match=error_match):
CONFIG_SCHEMA(config)
@pytest.mark.parametrize("variant", SUPPORTED_PSRAM_VARIANTS)
def test_psram_configuration_valid_supported_variants(
variant: str,
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: variant},
full_config={
CONF_ESPHOME: {},
"esp32": {
"variant": variant,
"cpu_frequency": "160MHz",
"framework": {"type": "esp-idf"},
},
},
)
"""Test that PSRAM configuration is valid on supported variants."""
from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
# This should not raise an exception
config = CONFIG_SCHEMA({})
FINAL_VALIDATE_SCHEMA(config)
def _setup_psram_final_validation_test(
esp32_config: dict,
set_core_config: SetCoreConfigCallable,
set_component_config: Any,
) -> str:
"""Helper function to set up ESP32 configuration for PSRAM final validation tests."""
# Use ESP32S3 for schema validation to allow all options, then override for final validation
schema_variant = "ESP32S3"
final_variant = esp32_config.get("variant", "ESP32S3")
full_esp32_config = {
"variant": final_variant,
"cpu_frequency": esp32_config.get("cpu_frequency", "240MHz"),
"framework": {"type": "esp-idf"},
}
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: schema_variant},
full_config={
CONF_ESPHOME: {},
"esp32": full_esp32_config,
},
)
set_component_config("esp32", full_esp32_config)
return final_variant
@pytest.mark.parametrize(
("config", "esp32_config", "expect_error", "error_match"),
[
pytest.param(
{"speed": "120MHz"},
{"cpu_frequency": "160MHz"},
True,
r"PSRAM 120MHz requires 240MHz CPU frequency",
id="120mhz_requires_240mhz_cpu",
),
pytest.param(
{"mode": "octal"},
{"variant": "ESP32"},
True,
r"Octal PSRAM is only supported on ESP32-S3",
id="octal_mode_only_esp32s3",
),
pytest.param(
{"mode": "quad", "enable_ecc": True},
{},
True,
r"ECC is only available in octal mode",
id="ecc_only_in_octal_mode",
),
pytest.param(
{"speed": "120MHZ"},
{"cpu_frequency": "240MHZ"},
False,
None,
id="120mhz_with_240mhz_cpu",
),
pytest.param(
{"mode": "octal"},
{"variant": "ESP32S3"},
False,
None,
id="octal_mode_on_esp32s3",
),
pytest.param(
{"mode": "octal", "enable_ecc": True},
{"variant": "ESP32S3"},
False,
None,
id="ecc_in_octal_mode",
),
],
)
def test_psram_final_validation(
config: Any,
esp32_config: dict,
expect_error: bool,
error_match: str | None,
set_core_config: SetCoreConfigCallable,
set_component_config: Any,
) -> None:
"""Test PSRAM final validation for both error and valid cases."""
from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
from esphome.core import CORE
final_variant = _setup_psram_final_validation_test(
esp32_config, set_core_config, set_component_config
)
validated_config = CONFIG_SCHEMA(config)
# Update CORE variant for final validation
CORE.data["esp32"][KEY_VARIANT] = final_variant
if expect_error:
with pytest.raises(cv.Invalid, match=error_match):
FINAL_VALIDATE_SCHEMA(validated_config)
else:
# This should not raise an exception
FINAL_VALIDATE_SCHEMA(validated_config)

View File

@@ -10,6 +10,36 @@ esphome:
data:
message: Button was pressed
- homeassistant.tag_scanned: pulse
- homeassistant.action:
action: weather.get_forecasts
data:
entity_id: weather.forecast_home
type: hourly
on_response:
- lambda: |-
if (response->is_success()) {
JsonObject json = response->get_json();
JsonObject next_hour = json["response"]["weather.forecast_home"]["forecast"][0];
float next_temperature = next_hour["temperature"].as<float>();
ESP_LOGD("main", "Next hour temperature: %f", next_temperature);
} else {
ESP_LOGE("main", "Action failed: %s", response->get_error_message().c_str());
}
- homeassistant.action:
action: weather.get_forecasts
data:
entity_id: weather.forecast_home
type: hourly
response_template: "{{ response['weather.forecast_home']['forecast'][0]['temperature'] }}"
on_response:
- lambda: |-
if (response->is_success()) {
JsonObject json = response->get_json();
float temperature = json["response"].as<float>();
ESP_LOGD("main", "Next hour temperature: %f", temperature);
} else {
ESP_LOGE("main", "Action failed: %s", response->get_error_message().c_str());
}
api:
port: 8000

View File

@@ -15,7 +15,7 @@ async def test_oversized_payload_plaintext(
run_compiled: RunCompiledFunction,
api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
"""Test that oversized payloads (>2304 bytes) from client cause disconnection without crashing."""
"""Test that oversized payloads (>100KiB) from client cause disconnection without crashing."""
process_exited = False
helper_log_found = False
@@ -39,8 +39,8 @@ async def test_oversized_payload_plaintext(
assert device_info is not None
assert device_info.name == "oversized-plaintext"
# Create an oversized payload (>2304 bytes which is our new limit)
oversized_data = b"X" * 3000 # ~3KiB, exceeds the 2304 byte limit
# Create an oversized payload (>100KiB)
oversized_data = b"X" * (100 * 1024 + 1) # 100KiB + 1 byte
# Access the internal connection to send raw data
frame_helper = client._connection._frame_helper
@@ -132,24 +132,22 @@ async def test_oversized_payload_noise(
run_compiled: RunCompiledFunction,
api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
"""Test that oversized payloads from client cause disconnection without crashing with noise encryption."""
"""Test that oversized payloads (>100KiB) from client cause disconnection without crashing with noise encryption."""
noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU="
process_exited = False
helper_log_found = False
cipherstate_failed = False
def check_logs(line: str) -> None:
nonlocal process_exited, helper_log_found
nonlocal process_exited, cipherstate_failed
# Check for signs that the process exited/crashed
if "Segmentation fault" in line or "core dumped" in line:
process_exited = True
# Check for HELPER_LOG message about message size exceeding maximum
# With our new protection, oversized messages are rejected at frame level
# Check for the expected warning about decryption failure
if (
"[VV]" in line
and "Bad packet: message size" in line
and "exceeds maximum" in line
"[W][api.connection" in line
and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line
):
helper_log_found = True
cipherstate_failed = True
async with run_compiled(yaml_config, line_callback=check_logs):
async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
@@ -161,8 +159,8 @@ async def test_oversized_payload_noise(
assert device_info is not None
assert device_info.name == "oversized-noise"
# Create an oversized payload (>2304 bytes which is our new limit)
oversized_data = b"Y" * 3000 # ~3KiB, exceeds the 2304 byte limit
# Create an oversized payload (>100KiB)
oversized_data = b"Y" * (100 * 1024 + 1) # 100KiB + 1 byte
# Access the internal connection to send raw data
frame_helper = client._connection._frame_helper
@@ -177,9 +175,9 @@ async def test_oversized_payload_noise(
# After disconnection, verify process didn't crash
assert not process_exited, "ESPHome process should not crash"
# Verify we saw the expected HELPER_LOG message
assert helper_log_found, (
"Expected to see HELPER_LOG about message size exceeding maximum"
# Verify we saw the expected warning message
assert cipherstate_failed, (
"Expected to see warning about CIPHERSTATE_DECRYPT_FAILED"
)
# Try to reconnect to verify the process is still running

View File

@@ -5,9 +5,6 @@ substitutions:
var21: '79'
value: 33
values: 44
position:
x: 79
y: 82
esphome:
name: test
@@ -29,7 +26,3 @@ test_list:
- Literal $values ${are not substituted}
- ["list $value", "${is not}", "${substituted}"]
- {"$dictionary": "$value", "${is not}": "${substituted}"}
- |-
{{{ "x", "79"}, { "y", "82"}}}
- '{{{"AA"}}}'
- '"HELLO"'

View File

@@ -8,9 +8,6 @@ substitutions:
var21: "79"
value: 33
values: 44
position:
x: 79
y: 82
test_list:
- "$var1"
@@ -30,7 +27,3 @@ test_list:
- !literal Literal $values ${are not substituted}
- !literal ["list $value", "${is not}", "${substituted}"]
- !literal {"$dictionary": "$value", "${is not}": "${substituted}"}
- |- # Test parsing things that look like a python set of sets when rendered:
{{{ "x", "${ position.x }"}, { "y", "${ position.y }"}}}
- ${ '{{{"AA"}}}' }
- ${ '"HELLO"' }

View File

@@ -661,45 +661,3 @@ class TestEsphomeCore:
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == Path(expected_default)
def test_platformio_cache_dir_with_env_var(self):
"""Test platformio_cache_dir when PLATFORMIO_CACHE_DIR env var is set."""
target = core.EsphomeCore()
test_cache_dir = "/custom/cache/dir"
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": test_cache_dir}):
assert target.platformio_cache_dir == test_cache_dir
def test_platformio_cache_dir_without_env_var(self):
"""Test platformio_cache_dir defaults to ~/.platformio/.cache."""
target = core.EsphomeCore()
with patch.dict(os.environ, {}, clear=True):
# Ensure env var is not set
os.environ.pop("PLATFORMIO_CACHE_DIR", None)
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_empty_env_var(self):
"""Test platformio_cache_dir with empty env var falls back to default."""
target = core.EsphomeCore()
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": ""}):
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_whitespace_env_var(self):
"""Test platformio_cache_dir with whitespace-only env var falls back to default."""
target = core.EsphomeCore()
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": " "}):
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_docker_addon_path(self):
"""Test platformio_cache_dir in Docker/HA addon environment."""
target = core.EsphomeCore()
addon_cache = "/data/cache/platformio"
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": addon_cache}):
assert target.platformio_cache_dir == addon_cache

View File

@@ -355,7 +355,6 @@ def test_clean_build(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock
mock_core.platformio_cache_dir = str(platformio_cache_dir)
# Verify all exist before
assert pioenvs_dir.exists()