1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-10 17:51:53 +00:00

Compare commits

..

10 Commits

Author SHA1 Message Date
J. Nick Koston
b4c0d89550 [logger] RP2040: Use write() with known length instead of println() 2025-12-21 12:20:50 -10:00
J. Nick Koston
1839f3f927 Merge branch 'dev' of https://github.com/esphome/esphome into dev 2025-12-21 12:12:44 -10:00
Steve
afbec0ba78 Merge branch 'dev' into dev 2025-12-21 12:46:44 +11:00
kabongsteve
97f311e84f Correct code update 2025-12-20 18:35:28 +11:00
kabongsteve
5528174590 Fix second location and compatibility issue 2025-12-20 18:01:15 +11:00
pre-commit-ci-lite[bot]
3ad6d860f0 [pre-commit.ci lite] apply automatic fixes 2025-12-20 05:54:51 +00:00
Steve
51ea938eea Merge branch 'dev' into dev 2025-12-20 16:53:01 +11:00
kabongsteve
2a1c24ba15 Update code after code review 2025-12-20 16:52:31 +11:00
kabongsteve
2eff8850c5 Fix clang-tidy issue 2025-12-17 16:25:03 +11:00
kabongsteve
26c4d749df Fix issue when esp_http_client_fetch_headers returns error 2025-12-17 15:50:41 +11:00
116 changed files with 1266 additions and 2858 deletions

View File

@@ -519,7 +519,6 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli

View File

@@ -780,13 +780,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
@@ -1266,17 +1259,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",

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation."""
from collections import defaultdict
import json
import sys
from . import (
@@ -298,28 +297,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

@@ -13,7 +13,6 @@
#include <cinttypes>
#include <functional>
#include <limits>
#include <new>
#include <utility>
#ifdef USE_ESP8266
#include <pgmspace.h>
@@ -97,7 +96,8 @@ static const int CAMERA_STOP_STREAM = 5000;
return;
#endif // USE_DEVICES
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) {
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto &noise_ctx = parent->get_noise_ctx();
if (noise_ctx.has_psk()) {
@@ -131,14 +131,11 @@ void APIConnection::start() {
this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
return;
}
// Initialize client name with peername (IP address) until Hello message provides actual name
char peername[socket::PEERNAME_MAX_LEN];
this->helper_->getpeername_to(peername);
this->client_info_.name = peername;
this->client_info_.peername = helper_->getpeername();
this->client_info_.name = this->client_info_.peername;
}
APIConnection::~APIConnection() {
this->destroy_active_iterator_();
#ifdef USE_BLUETOOTH_PROXY
if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
@@ -151,32 +148,6 @@ APIConnection::~APIConnection() {
#endif
}
void APIConnection::destroy_active_iterator_() {
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
this->iterator_storage_.list_entities.~ListEntitiesIterator();
break;
case ActiveIterator::INITIAL_STATE:
this->iterator_storage_.initial_state.~InitialStateIterator();
break;
case ActiveIterator::NONE:
break;
}
this->active_iterator_ = ActiveIterator::NONE;
}
void APIConnection::begin_iterator_(ActiveIterator type) {
this->destroy_active_iterator_();
this->active_iterator_ = type;
if (type == ActiveIterator::LIST_ENTITIES) {
new (&this->iterator_storage_.list_entities) ListEntitiesIterator(this);
this->iterator_storage_.list_entities.begin();
} else {
new (&this->iterator_storage_.initial_state) InitialStateIterator(this);
this->iterator_storage_.initial_state.begin();
}
}
void APIConnection::loop() {
if (this->flags_.next_close) {
// requested a disconnect
@@ -219,42 +190,31 @@ void APIConnection::loop() {
this->process_batch_();
}
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
if (this->iterator_storage_.list_entities.completed()) {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
if (!this->list_entities_iterator_.completed()) {
this->process_iterator_batch_(this->list_entities_iterator_);
} else if (!this->initial_state_iterator_.completed()) {
this->process_iterator_batch_(this->initial_state_iterator_);
// If we've completed initial states, process any remaining and clear the flag
if (this->initial_state_iterator_.completed()) {
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
break;
case ActiveIterator::INITIAL_STATE:
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
break;
case ActiveIterator::NONE:
break;
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
}
}
if (this->flags_.sent_ping) {
// Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
on_fatal_error();
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting"));
ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(),
this->client_info_.peername.c_str());
}
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
// Only send ping if we're not disconnecting
@@ -271,24 +231,40 @@ void APIConnection::loop() {
}
}
#ifdef USE_CAMERA
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
#endif
if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
this->image_reader_->consume_data(to_send);
if (done) {
this->image_reader_->return_image();
}
}
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
if (state_subs_at_ >= 0) {
this->process_state_subscriptions_();
}
#endif
#ifdef USE_CAMERA
// Process camera last - state updates are higher priority
// (missing a frame is fine, missing a state update is not)
this->try_send_camera_image_();
#endif
}
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
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected"));
ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
this->flags_.next_close = true;
DisconnectResponse resp;
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
@@ -1084,36 +1060,6 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
#endif
#ifdef USE_CAMERA
void APIConnection::try_send_camera_image_() {
if (!this->image_reader_)
return;
// Send as many chunks as possible without blocking
while (this->image_reader_->available()) {
if (!this->helper_->can_write_without_blocking())
return;
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
#endif
if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
return; // Send failed, try again later
}
this->image_reader_->consume_data(to_send);
if (done) {
this->image_reader_->return_image();
return;
}
}
}
void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
if (!this->flags_.state_subscription)
return;
@@ -1121,11 +1067,8 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image)
return;
if (this->image_reader_->available())
return;
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) {
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE))
this->image_reader_->set_image(std::move(image));
// Try to send immediately to reduce latency
this->try_send_camera_image_();
}
}
uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -1506,10 +1449,9 @@ void APIConnection::complete_authentication_() {
}
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
// Trigger expects std::string, get fresh peername from socket
this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->helper_->getpeername());
this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1525,12 +1467,11 @@ void APIConnection::complete_authentication_() {
bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len);
this->client_info_.peername = this->helper_->getpeername();
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::PEERNAME_MAX_LEN];
this->helper_->getpeername_to(peername);
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(),
peername, this->client_api_version_major_, this->client_api_version_minor_);
this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
resp.api_version_major = 1;
@@ -1851,12 +1792,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
#ifdef USE_API_PASSWORD
void APIConnection::on_unauthenticated_access() {
this->on_fatal_error();
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no authentication"));
ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
}
#endif
void APIConnection::on_no_setup_connection() {
this->on_fatal_error();
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
}
void APIConnection::on_fatal_error() {
this->helper_->close();
@@ -2104,18 +2045,9 @@ void APIConnection::process_state_subscriptions_() {
}
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::PEERNAME_MAX_LEN];
this->helper_->getpeername_to(peername);
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->client_info_.name.c_str(), peername,
LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) {
char peername[socket::PEERNAME_MAX_LEN];
this->helper_->getpeername_to(peername);
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), peername, LOG_STR_ARG(message),
LOG_STR_ARG(api_error_to_logstr(err)), errno);
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);
}
} // namespace esphome::api

View File

@@ -17,9 +17,8 @@ namespace esphome::api {
// Client information structure
struct ClientInfo {
std::string name; // Client name from Hello message
// Note: peername (IP address) is not stored here to save memory.
// Use helper_->getpeername_to() or helper_->getpeername() when needed.
std::string name; // Client name from Hello message
std::string peername; // IP:port from socket
};
// Keepalive timeout in milliseconds
@@ -209,14 +208,10 @@ class APIConnection final : public APIServerConnection {
bool send_disconnect_response(const DisconnectRequest &msg) override;
bool send_ping_response(const PingRequest &msg) override;
bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
void subscribe_states(const SubscribeStatesRequest &msg) override {
this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes
if (this->active_iterator_ == ActiveIterator::NONE) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
this->initial_state_iterator_.begin();
}
void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->flags_.log_subscription = msg.level;
@@ -291,21 +286,12 @@ class APIConnection final : public APIServerConnection {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const std::string &get_name() const { return this->client_info_.name; }
/// Get peer name (IP address) into a stack buffer - avoids heap allocation
size_t get_peername_to(std::span<char, socket::PEERNAME_MAX_LEN> buf) const {
return this->helper_->getpeername_to(buf);
}
/// Get peer name as std::string - use sparingly, allocates on heap
std::string get_peername() const { return this->helper_->getpeername(); }
const std::string &get_peername() const { return this->client_info_.peername; }
protected:
// Helper function to handle authentication completion
void complete_authentication_();
#ifdef USE_CAMERA
void try_send_camera_image_();
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_();
#endif
@@ -329,10 +315,17 @@ class APIConnection final : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
// Get object_id with zero heap allocation
// Static case returns direct reference, dynamic case uses buffer
char object_id_buf[OBJECT_ID_MAX_LEN];
msg.set_object_id(entity->get_object_id_to(object_id_buf));
// Try to use static reference first to avoid allocation
StringRef static_ref = entity->get_object_id_ref_for_api_();
// Store dynamic string outside the if-else to maintain lifetime
std::string object_id;
if (!static_ref.empty()) {
msg.set_object_id(static_ref);
} else {
// Dynamic case - need to allocate
object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
}
if (entity->has_own_name()) {
msg.set_name(entity->get_name());
@@ -508,22 +501,10 @@ class APIConnection final : public APIServerConnection {
std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
// These iterators are never active simultaneously - list_entities runs to completion
// before initial_state begins, so we use a union with explicit construction/destruction.
enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE };
union IteratorUnion {
ListEntitiesIterator list_entities;
InitialStateIterator initial_state;
// Constructor/destructor do nothing - use placement new/explicit destructor
IteratorUnion() {}
~IteratorUnion() {}
} iterator_storage_;
// Helper methods for iterator lifecycle management
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
// Group 2: Larger objects (must be 4-byte aligned)
// These contain vectors/pointers internally, so putting them early ensures good alignment
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
@@ -638,9 +619,7 @@ class APIConnection final : public APIServerConnection {
// 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0};
// 1-byte type to fill padding
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
// Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical
@@ -758,8 +737,6 @@ class APIConnection final : public APIServerConnection {
return this->schedule_batch_();
}
// Helper function to log client messages with name and peername
void log_client_(int level, const LogString *message);
// Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err);
// Helper to handle fatal errors with logging

View File

@@ -13,16 +13,8 @@ namespace esphome::api {
static const char *const TAG = "api.frame_helper";
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername__[socket::PEERNAME_MAX_LEN]; \
this->socket_->getpeername_to(peername__); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), peername__, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.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())

View File

@@ -91,7 +91,6 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
size_t getpeername_to(std::span<char, socket::PEERNAME_MAX_LEN> buf) { return socket_->getpeername_to(buf); }
APIError close() {
state_ = State::CLOSED;
int err = this->socket_->close();

View File

@@ -24,16 +24,8 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername__[socket::PEERNAME_MAX_LEN]; \
this->socket_->getpeername_to(peername__); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), peername__, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.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())

View File

@@ -18,16 +18,8 @@ namespace esphome::api {
static const char *const TAG = "api.plaintext";
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername__[socket::PEERNAME_MAX_LEN]; \
this->socket_->getpeername_to(peername__); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), peername__, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.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())

View File

@@ -125,18 +125,15 @@ void APIServer::loop() {
if (!sock)
break;
char peername[socket::PEERNAME_MAX_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str());
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
@@ -169,7 +166,8 @@ void APIServer::loop() {
// Network is down - disconnect all clients
for (auto &client : this->clients_) {
client->on_fatal_error();
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
client->client_info_.peername.c_str());
}
// Continue to process and clean up the clients below
}
@@ -187,8 +185,7 @@ void APIServer::loop() {
// Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Trigger expects std::string, get fresh peername from socket
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->get_peername());
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
@@ -428,7 +425,7 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
std::function<void(const std::string &)> f, bool once) {
std::function<void(std::string)> f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
@@ -437,7 +434,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri
// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once) {
std::function<void(std::string)> f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
@@ -457,23 +454,23 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(const std::string &)> f) {
std::function<void(std::string)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(const std::string &)> f) {
std::function<void(std::string)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// Existing std::string overload (for custom_api_device.h - heap allocation)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(std::string)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(std::string)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}

View File

@@ -195,7 +195,7 @@ class APIServer : public Component,
struct HomeAssistantStateSubscription {
const char *entity_id; // Pointer to flash (internal) or heap (external)
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
std::function<void(const std::string &)> callback;
std::function<void(std::string)> callback;
bool once;
// Dynamic storage for external components using std::string API (custom_api_device.h)
@@ -205,16 +205,14 @@ class APIServer : public Component,
};
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(const std::string &)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(const std::string &)> f);
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
// Existing std::string overload (for custom_api_device.h - heap allocation)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(std::string)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
@@ -238,10 +236,10 @@ class APIServer : public Component,
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(const std::string &)> f,
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(std::string)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once);
std::function<void(std::string)> f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;

View File

@@ -122,7 +122,7 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature");
* }
*
* void on_state_changed(const std::string &state) {
* void on_state_changed(std::string state) {
* // State of sensor.weather_forecast is `state`
* }
* ```
@@ -133,7 +133,7 @@ class CustomAPIDevice {
* @param attribute The entity state attribute to track.
*/
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &), const std::string &entity_id,
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
@@ -148,7 +148,7 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
* }
*
* void on_state_changed(const std::string &entity_id, const std::string &state) {
* void on_state_changed(std::string entity_id, std::string state) {
* // State of `entity_id` is `state`
* }
* ```
@@ -159,14 +159,14 @@ class CustomAPIDevice {
* @param attribute The entity state attribute to track.
*/
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, const std::string &),
const std::string &entity_id, const std::string &attribute = "") {
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
}
#else
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &), const std::string &entity_id,
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
@@ -174,8 +174,8 @@ class CustomAPIDevice {
}
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, const std::string &),
const std::string &entity_id, const std::string &attribute = "") {
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");

View File

@@ -1,7 +1,12 @@
import esphome.codegen as cg
from esphome.components import climate
from esphome.components import ble_client, climate
import esphome.config_validation as cv
from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE
from esphome.const import (
CONF_HEAT_MODE,
CONF_RECEIVE_TIMEOUT,
CONF_TEMPERATURE_SOURCE,
CONF_TIME_ID,
)
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
@@ -33,6 +38,22 @@ CONFIG_SCHEMA = (
}
)
.extend(cv.polling_component_schema("60s"))
.extend(
# TODO: remove compat layer.
{
cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid(
"The 'ble_client_id' option has been removed. Please migrate "
"to the new `bedjet_id` option in the `bedjet` component.\n"
"See https://esphome.io/components/climate/bedjet/"
),
cv.Optional(CONF_TIME_ID): cv.invalid(
"The 'time_id' option has been moved to the `bedjet` component."
),
cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid(
"The 'receive_timeout' option has been moved to the `bedjet` component."
),
}
)
.extend(BEDJET_CLIENT_SCHEMA)
)

View File

@@ -1,8 +1,8 @@
#include "bh1750.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::bh1750 {
namespace esphome {
namespace bh1750 {
static const char *const TAG = "bh1750.sensor";
@@ -13,31 +13,6 @@ static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011;
static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000;
static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001;
static constexpr uint32_t MEASUREMENT_TIMEOUT_MS = 2000;
static constexpr float HIGH_LIGHT_THRESHOLD_LX = 7000.0f;
// Measurement time constants (datasheet values)
static constexpr uint16_t MTREG_DEFAULT = 69;
static constexpr uint16_t MTREG_MIN = 31;
static constexpr uint16_t MTREG_MAX = 254;
static constexpr uint16_t MEAS_TIME_L_MS = 24; // L-resolution max measurement time @ mtreg=69
static constexpr uint16_t MEAS_TIME_H_MS = 180; // H/H2-resolution max measurement time @ mtreg=69
// Conversion constants (datasheet formulas)
static constexpr float RESOLUTION_DIVISOR = 1.2f; // counts to lux conversion divisor
static constexpr float MODE_H2_DIVISOR = 2.0f; // H2 mode has 2x higher resolution
// MTreg calculation constants
static constexpr int COUNTS_TARGET = 50000; // Target counts for optimal range (avoid saturation)
static constexpr int COUNTS_NUMERATOR = 10;
static constexpr int COUNTS_DENOMINATOR = 12;
// MTreg register bit manipulation constants
static constexpr uint8_t MTREG_HI_SHIFT = 5; // High 3 bits start at bit 5
static constexpr uint8_t MTREG_HI_MASK = 0b111; // 3-bit mask for high bits
static constexpr uint8_t MTREG_LO_SHIFT = 0; // Low 5 bits start at bit 0
static constexpr uint8_t MTREG_LO_MASK = 0b11111; // 5-bit mask for low bits
/*
bh1750 properties:
@@ -68,7 +43,74 @@ void BH1750Sensor::setup() {
this->mark_failed();
return;
}
this->state_ = IDLE;
}
void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f) {
// turn on (after one-shot sensor automatically powers down)
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
f(NAN);
return;
}
if (active_mtreg_ != mtreg) {
// set mtreg
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
active_mtreg_ = 0;
f(NAN);
return;
}
active_mtreg_ = mtreg;
}
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = 24 * mtreg / 69;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = 180 * mtreg / 69;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = 180 * mtreg / 69;
break;
default:
f(NAN);
return;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
f(NAN);
return;
}
// probably not needed, but adjust for rounding
meas_time++;
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
f(NAN);
return;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / 1.2f;
lx *= 69.0f / mtreg;
if (mode == BH1750_MODE_H2)
lx /= 2.0f;
f(lx);
});
}
void BH1750Sensor::dump_config() {
@@ -82,189 +124,45 @@ void BH1750Sensor::dump_config() {
}
void BH1750Sensor::update() {
const uint32_t now = millis();
// Start coarse measurement to determine optimal mode/mtreg
if (this->state_ != IDLE) {
// Safety timeout: reset if stuck
if (now - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Measurement timeout, resetting state");
this->state_ = IDLE;
} else {
ESP_LOGW(TAG, "Previous measurement not complete, skipping update");
// first do a quick measurement in L-mode with full range
// to find right range
this->read_lx_(BH1750_MODE_L, 31, [this](float val) {
if (std::isnan(val)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
}
if (!this->start_measurement_(BH1750_MODE_L, MTREG_MIN, now)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
this->state_ = WAITING_COARSE_MEASUREMENT;
this->enable_loop(); // Enable loop while measurement in progress
}
void BH1750Sensor::loop() {
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case IDLE:
// Disable loop when idle to save cycles
this->disable_loop();
break;
case WAITING_COARSE_MEASUREMENT:
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
this->state_ = READING_COARSE_RESULT;
}
break;
case READING_COARSE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
this->process_coarse_result_(lx);
// Start fine measurement with optimal settings
// fetch millis() again since the read can take a bit
if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, millis())) {
this->fail_and_reset_();
break;
}
this->state_ = WAITING_FINE_MEASUREMENT;
break;
BH1750Mode use_mode;
uint8_t use_mtreg;
if (val <= 7000) {
use_mode = BH1750_MODE_H2;
use_mtreg = 254;
} else {
use_mode = BH1750_MODE_H;
// lx = counts / 1.2 * (69 / mtreg)
// -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
// -> mtreg = 50000*(10/12)*(69/lx)
int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val);
use_mtreg = std::min(254, std::max(31, ideal_mtreg));
}
ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg);
case WAITING_FINE_MEASUREMENT:
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
this->state_ = READING_FINE_RESULT;
this->read_lx_(use_mode, use_mtreg, [this](float val) {
if (std::isnan(val)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
break;
case READING_FINE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
this->status_clear_warning();
this->publish_state(lx);
this->state_ = IDLE;
break;
}
}
}
bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now) {
// Power on
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
return false;
}
// Set MTreg if changed
if (this->active_mtreg_ != mtreg) {
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> MTREG_HI_SHIFT) & MTREG_HI_MASK);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> MTREG_LO_SHIFT) & MTREG_LO_MASK);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
this->active_mtreg_ = 0;
return false;
}
this->active_mtreg_ = mtreg;
}
// Start measurement
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = MEAS_TIME_L_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
default:
return false;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
return false;
}
// Store current measurement parameters
this->current_mode_ = mode;
this->current_mtreg_ = mtreg;
this->measurement_start_time_ = now;
this->measurement_duration_ = meas_time + 1; // Add 1ms for safety
return true;
}
bool BH1750Sensor::read_measurement_(float &lx_out) {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
return false;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / RESOLUTION_DIVISOR;
lx *= float(MTREG_DEFAULT) / this->current_mtreg_;
if (this->current_mode_ == BH1750_MODE_H2) {
lx /= MODE_H2_DIVISOR;
}
lx_out = lx;
return true;
}
void BH1750Sensor::process_coarse_result_(float lx) {
if (std::isnan(lx)) {
// Use defaults if coarse measurement failed
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
return;
}
if (lx <= HIGH_LIGHT_THRESHOLD_LX) {
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
} else {
this->fine_mode_ = BH1750_MODE_H;
// lx = counts / 1.2 * (69 / mtreg)
// -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
// -> mtreg = 50000*(10/12)*(69/lx)
int ideal_mtreg = COUNTS_TARGET * COUNTS_NUMERATOR * MTREG_DEFAULT / (COUNTS_DENOMINATOR * (int) lx);
this->fine_mtreg_ = std::min((int) MTREG_MAX, std::max((int) MTREG_MIN, ideal_mtreg));
}
ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_);
}
void BH1750Sensor::fail_and_reset_() {
this->status_set_warning();
this->publish_state(NAN);
this->state_ = IDLE;
this->publish_state(val);
});
});
}
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace esphome::bh1750
} // namespace bh1750
} // namespace esphome

View File

@@ -4,9 +4,10 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::bh1750 {
namespace esphome {
namespace bh1750 {
enum BH1750Mode : uint8_t {
enum BH1750Mode {
BH1750_MODE_L,
BH1750_MODE_H,
BH1750_MODE_H2,
@@ -20,36 +21,13 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c:
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
float get_setup_priority() const override;
protected:
// State machine states
enum State : uint8_t {
IDLE,
WAITING_COARSE_MEASUREMENT,
READING_COARSE_RESULT,
WAITING_FINE_MEASUREMENT,
READING_FINE_RESULT,
};
void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f);
// 4-byte aligned members
uint32_t measurement_start_time_{0};
uint32_t measurement_duration_{0};
// 1-byte members grouped together to minimize padding
State state_{IDLE};
BH1750Mode current_mode_{BH1750_MODE_L};
uint8_t current_mtreg_{31};
BH1750Mode fine_mode_{BH1750_MODE_H2};
uint8_t fine_mtreg_{254};
uint8_t active_mtreg_{0};
// Helper methods
bool start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now);
bool read_measurement_(float &lx_out);
void process_coarse_result_(float lx);
void fail_and_reset_();
};
} // namespace esphome::bh1750
} // namespace bh1750
} // namespace esphome

View File

@@ -20,6 +20,16 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Optional("resolution"): cv.invalid(
"The 'resolution' option has been removed. The optimal value is now dynamically calculated."
),
cv.Optional("measurement_duration"): cv.invalid(
"The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated."
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x23))
)

View File

@@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
def AUTO_LOAD() -> list[str]:
auto_load = ["web_server_base", "ota.web_server"]
if CORE.is_esp32:
if CORE.using_esp_idf:
auto_load.append("socket")
return auto_load

View File

@@ -63,13 +63,11 @@ def validate_auto_clear(value):
return cv.boolean(value)
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a basic display schema with configurable default update interval."""
return cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema(default_update_interval))
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config):
@@ -83,41 +81,34 @@ def _validate_test_card(config):
return config
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a full display schema with configurable default update interval."""
schema = basic_display_schema(default_update_interval).extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
schema.add_extra(_validate_test_card)
return schema
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):

View File

@@ -31,7 +31,6 @@ from esphome.const import (
CONF_TRANSFORM,
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
SCHEDULER_DONT_RUN,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
@@ -73,10 +72,12 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
minimum_update_interval = update_interval(
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.full_display_schema("60s")
.extend(
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
@@ -93,6 +94,9 @@ def model_schema(config):
{
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
update_interval, cv.Range(min=minimum_update_interval)
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
@@ -146,22 +150,15 @@ def _final_validate(config):
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
# If no drawing methods are configured, and LVGL is not enabled, show a test card
if (
CONF_LAMBDA not in config
and CONF_PAGES not in config
and LVGL_DOMAIN not in global_config
):
config[CONF_SHOW_TEST_CARD] = True
interval = config[CONF_UPDATE_INTERVAL]
if interval != SCHEDULER_DONT_RUN:
model = MODELS[config[CONF_MODEL]]
minimum = update_interval(model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s"))
if interval < minimum:
raise cv.Invalid(
f"update_interval must be at least {minimum} for {model.name}, got {interval}"
)
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
return config

View File

@@ -606,9 +606,6 @@ CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
# Ring buffer IRAM requirement tracking
KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram"
def require_vfs_select() -> None:
"""Mark that VFS select support is required by a component.
@@ -628,17 +625,6 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True
def enable_ringbuf_in_iram() -> None:
"""Keep ring buffer functions in IRAM instead of moving them to flash.
Call this from components that use esphome/core/ring_buffer.cpp and need
the ring buffer functions to remain in IRAM for performance reasons
(e.g., voice assistants, audio components).
This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled.
"""
CORE.data[KEY_RINGBUF_IN_IRAM] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *)
@@ -1041,18 +1027,14 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True)
# Place ring buffer functions into flash instead of IRAM by default
# This saves IRAM but may impact performance for audio/voice components.
# Components that need ring buffer in IRAM call enable_ringbuf_in_iram().
# Users can also set ringbuf_in_iram: true to force IRAM placement.
# In ESP-IDF 6.0 flash placement becomes the default.
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM] or CORE.data.get(
KEY_RINGBUF_IN_IRAM, False
):
# User config or component requires ring buffer in IRAM for performance
# This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default.
# Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues.
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]:
# User requests ring buffer in IRAM
# IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False)
else:
# No component needs it - place in flash to save IRAM
# Place in flash to save IRAM (default)
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True)
# Setup watchdog

View File

@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
break;
}
gpio_set_intr_type(this->get_pin_num(), idf_type);
gpio_intr_enable(this->get_pin_num());
if (!isr_service_installed) {
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
if (res != ESP_OK) {
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
isr_service_installed = true;
}
gpio_isr_handler_add(this->get_pin_num(), func, arg);
gpio_intr_enable(this->get_pin_num());
}
std::string ESP32InternalGPIOPin::dump_summary() const {

View File

@@ -96,10 +96,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();
@@ -260,11 +256,8 @@ bool ESP32BLE::ble_setup_() {
}
#endif
// BLE device names are limited to 20 characters
// Buffer: 20 chars + null terminator
constexpr size_t ble_name_max_len = 21;
char name_buffer[ble_name_max_len];
const char *device_name;
std::string name_with_suffix;
if (this->name_ != nullptr) {
if (App.is_name_add_mac_suffix_enabled()) {
@@ -275,28 +268,23 @@ bool ESP32BLE::ble_setup_() {
char mac_addr[mac_address_len];
get_mac_address_into_buffer(mac_addr);
const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len;
make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr,
mac_address_suffix_len);
device_name = name_buffer;
name_with_suffix =
make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len);
device_name = name_with_suffix.c_str();
} else {
device_name = this->name_;
}
} else {
const std::string &app_name = App.get_name();
size_t name_len = app_name.length();
if (name_len > 20) {
name_with_suffix = App.get_name();
if (name_with_suffix.length() > 20) {
if (App.is_name_add_mac_suffix_enabled()) {
// Keep first 13 chars and last 7 chars (MAC suffix), remove middle
memcpy(name_buffer, app_name.c_str(), 13);
memcpy(name_buffer + 13, app_name.c_str() + name_len - 7, 7);
name_with_suffix.erase(13, name_with_suffix.length() - 20);
} else {
memcpy(name_buffer, app_name.c_str(), 20);
name_with_suffix.resize(20);
}
name_buffer[20] = '\0';
} else {
memcpy(name_buffer, app_name.c_str(), name_len + 1); // Include null terminator
}
device_name = name_buffer;
device_name = name_with_suffix.c_str();
}
err = esp_ble_gap_set_device_name(device_name);

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

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

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
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
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
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
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -2,7 +2,7 @@ import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c, socket
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv
@@ -27,7 +27,7 @@ import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["camera", "socket"]
AUTO_LOAD = ["camera"]
DEPENDENCIES = ["esp32"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
@@ -324,7 +324,6 @@ SETTERS = {
async def to_code(config):
cg.add_define("USE_CAMERA")
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config, "camera")
await cg.register_component(var, config)

View File

@@ -11,7 +11,6 @@ namespace esphome {
namespace esp32_camera {
static const char *const TAG = "esp32_camera";
static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792;
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
#endif
@@ -43,12 +42,12 @@ void ESP32Camera::setup() {
this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task,
"framebuffer_task", // name
FRAMEBUFFER_TASK_STACK_SIZE, // stack size
this, // task pv params
1, // priority
nullptr, // handle
1 // core
"framebuffer_task", // name
1024, // stack size
this, // task pv params
1, // priority
nullptr, // handle
1 // core
);
}
@@ -168,19 +167,6 @@ void ESP32Camera::dump_config() {
}
void ESP32Camera::loop() {
// Fast path: skip all work when truly idle
// (no current image, no pending requests, and not time for idle request yet)
const uint32_t now = App.get_loop_component_start_time();
if (!this->current_image_ && !this->has_requested_image_()) {
// Only check idle interval when we're otherwise idle
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now;
this->request_image(camera::IDLE);
} else {
return;
}
}
// check if we can return the image
if (this->can_return_image_()) {
// return image
@@ -189,6 +175,13 @@ void ESP32Camera::loop() {
this->current_image_.reset();
}
// request idle image every idle_update_interval
const uint32_t now = App.get_loop_component_start_time();
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now;
this->request_image(camera::IDLE);
}
// Check if we should fetch a new image
if (!this->has_requested_image_())
return;
@@ -428,10 +421,6 @@ void ESP32Camera::framebuffer_task(void *pv) {
while (true) {
camera_fb_t *framebuffer = esp_camera_fb_get();
xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY);
// Only wake the main loop if there's a pending request to consume the frame
if (that->has_requested_image_()) {
App.wake_loop_threadsafe();
}
// return is no-op for config with 1 fb
xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY);
esp_camera_fb_return(framebuffer);

View File

@@ -2,7 +2,6 @@
#ifdef USE_ESP32
#include <atomic>
#include <esp_camera.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
@@ -206,8 +205,8 @@ class ESP32Camera : public camera::Camera {
esp_err_t init_error_{ESP_OK};
std::shared_ptr<ESP32CameraImage> current_image_;
std::atomic<uint8_t> single_requesters_{0};
std::atomic<uint8_t> stream_requesters_{0};
uint8_t single_requesters_{0};
uint8_t stream_requesters_{0};
QueueHandle_t framebuffer_get_queue_;
QueueHandle_t framebuffer_return_queue_;
std::vector<camera::CameraListener *> listeners_;

View File

@@ -220,6 +220,10 @@ BASE_SCHEMA = cv.Schema(
cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA,
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional("enable_mdns"): cv.invalid(
"This option has been removed. Please use the [disabled] option under the "
"new mdns component instead."
),
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
}
).extend(cv.COMPONENT_SCHEMA)

View File

@@ -71,7 +71,7 @@ void FanCall::validate_() {
auto traits = this->parent_.get_traits();
if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"

View File

@@ -11,7 +11,7 @@ namespace fan {
class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
@@ -23,9 +23,9 @@ class FanTraits {
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
/// Return how many speed levels the fan has
uint8_t supported_speed_count() const { return this->speed_count_; }
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
@@ -64,7 +64,7 @@ class FanTraits {
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
uint8_t speed_count_{};
int speed_count_{};
std::vector<const char *> preset_modes_{};
};

View File

@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}

View File

@@ -15,7 +15,7 @@ enum DecayMode {
class HBridgeFan : public Component, public fan::Fan {
public:
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
output::FloatOutput *pin_b_;
output::FloatOutput *enable_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};

View File

@@ -152,14 +152,26 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->set_response_headers(user_data.response_headers);
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
int64_t result = esp_http_client_fetch_headers(client);
if (result < 0) {
if (result == -ESP_ERR_HTTP_EAGAIN) {
container->status_code = ESP_ERR_HTTP_EAGAIN;
} else {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(result));
esp_http_client_cleanup(client);
return nullptr;
}
} else {
container->content_length = result;
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->set_response_headers(user_data.response_headers);
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
}
}
if (this->follow_redirects_) {
@@ -187,15 +199,26 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
result = esp_http_client_fetch_headers(client);
if (result < 0) {
if (result == -ESP_ERR_HTTP_EAGAIN) {
container->status_code = ESP_ERR_HTTP_EAGAIN;
} else {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(result));
esp_http_client_cleanup(client);
return nullptr;
}
} else {
container->content_length = result;
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
}
}
num_redirects--;
}

View File

@@ -146,7 +146,7 @@ def _final_validate(config):
full_config = fv.full_config.get()[CONF_I2C]
if CORE.using_zephyr and len(full_config) > 1:
raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES:
if CORE.using_esp_idf and get_esp32_variant() in ESP32_I2C_CAPABILITIES:
variant = get_esp32_variant()
max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"]
if len(full_config) > max_num:
@@ -237,6 +237,10 @@ def i2c_device_schema(default_address):
"""
schema = {
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus),
cv.Optional("multiplexer"): cv.invalid(
"This option has been removed, please see "
"the tca9584a docs for the updated way to use multiplexers"
),
}
if default_address is None:
schema[cv.Required(CONF_ADDRESS)] = cv.i2c_address

View File

@@ -116,7 +116,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

@@ -1,11 +1,6 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
enable_ringbuf_in_iram,
get_esp32_variant,
)
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
@@ -15,6 +10,8 @@ from esphome.components.esp32.const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_sdkconfig_option,
get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
@@ -235,8 +232,6 @@ def validate_use_legacy(value):
if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino):
raise cv.Invalid("Arduino supports only the legacy i2s driver")
_set_use_legacy_driver(value[CONF_USE_LEGACY])
elif CORE.using_arduino:
_set_use_legacy_driver(True)
return value
@@ -266,7 +261,8 @@ def _final_validate(_):
def use_legacy():
return _get_use_legacy_driver()
legacy_driver = _get_use_legacy_driver()
return not (CORE.using_esp_idf and not legacy_driver)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -281,9 +277,6 @@ async def to_code(config):
# Helps avoid callbacks being skipped due to processor load
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
# Keep ring buffer functions in IRAM for audio performance
enable_ringbuf_in_iram()
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config:
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))

View File

@@ -26,7 +26,7 @@ def validate_logger(config):
logger_conf = fv.full_config.get()[CONF_LOGGER]
if logger_conf[CONF_BAUD_RATE] == 0:
raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0")
if CORE.is_esp32 and (
if CORE.using_esp_idf and (
logger_conf[CONF_HARDWARE_UART] == USB_CDC
and get_esp32_variant() == VARIANT_ESP32S3
):

View File

@@ -118,11 +118,11 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Platform-specific: does write_msg_ add its own newline?
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, RP2040, LibreTiny, Zephyr)
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, RP2040, LibreTiny)
// Allows single write call with newline included for efficiency
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
// Newline should NOT be added to buffer
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
#else
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;

View File

@@ -6,7 +6,6 @@
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/printk.h>
#include <zephyr/usb/usb_device.h>
namespace esphome::logger {
@@ -15,7 +14,7 @@ static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC
void Logger::loop() {
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
return;
}
static bool opened = false;
@@ -63,17 +62,18 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Single write with newline already in buffer (added by caller)
void HOT Logger::write_msg_(const char *msg, size_t) {
#ifdef CONFIG_PRINTK
k_str_out(const_cast<char *>(msg), len);
printk("%s\n", msg);
#endif
if (this->uart_dev_ == nullptr) {
if (nullptr == this->uart_dev_) {
return;
}
for (size_t i = 0; i < len; ++i) {
uart_poll_out(this->uart_dev_, msg[i]);
while (*msg) {
uart_poll_out(this->uart_dev_, *msg);
++msg;
}
uart_poll_out(this->uart_dev_, '\n');
}
const LogString *Logger::get_uart_selection_() {

View File

@@ -256,11 +256,9 @@ async def to_code(configs):
True,
type=lv_font_t.operator("ptr").operator("const"),
)
# static=False because LV_FONT_CUSTOM_DECLARE creates an extern declaration
cg.new_variable(
globfont_id,
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
static=False,
)
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
else:

View File

@@ -337,7 +337,7 @@ def lv_Pvariable(type, name) -> MockObj:
"""
if isinstance(name, str):
name = ID(name, True, type)
decl = VariableDeclarationExpression(type, "*", name, static=True)
decl = VariableDeclarationExpression(type, "*", name)
CORE.add_global(decl)
var = MockObj(name, "->")
CORE.register_variable(name, var)
@@ -353,7 +353,7 @@ def lv_variable(type, name) -> MockObj:
"""
if isinstance(name, str):
name = ID(name, True, type)
decl = VariableDeclarationExpression(type, "", name, static=True)
decl = VariableDeclarationExpression(type, "", name)
CORE.add_global(decl)
var = MockObj(name, ".")
CORE.register_variable(name, var)

View File

@@ -133,7 +133,7 @@ async def to_code(config):
value_type,
)
var = MockObj(varid, ".")
decl = VariableDeclarationExpression(varid.type, "", varid, static=True)
decl = VariableDeclarationExpression(varid.type, "", varid)
add_global(decl)
CORE.register_variable(varid, var)

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

@@ -157,12 +157,14 @@ async def to_code(config):
return
if CORE.using_arduino:
if CORE.is_esp8266:
if CORE.is_esp32:
cg.add_library("ESPmDNS", None)
elif CORE.is_esp8266:
cg.add_library("ESP8266mDNS", None)
elif CORE.is_rp2040:
cg.add_library("LEAmDNS", None)
if CORE.is_esp32:
if CORE.using_esp_idf:
add_idf_component(name="espressif/mdns", ref="1.9.1")
cg.add_define("USE_MDNS")

View File

@@ -448,9 +448,6 @@ async def to_code(config):
# The inference task queues detection events that need immediate processing
socket.require_wake_loop_threadsafe()
# Keep ring buffer functions in IRAM for audio performance
esp32.enable_ringbuf_in_iram()
mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE])
cg.add(var.set_microphone_source(mic_source))

View File

@@ -156,7 +156,7 @@ async def to_code(config):
"High performance networking disabled by user configuration (overriding component request)"
)
if CORE.is_esp32 and should_enable:
if CORE.is_esp32 and CORE.using_esp_idf and should_enable:
# Check if PSRAM is guaranteed (set by psram component during final validation)
psram_guaranteed = psram_is_guaranteed()
@@ -210,12 +210,12 @@ async def to_code(config):
"USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT]
)
if CORE.is_esp32:
if CORE.using_arduino:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", True)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", True)
else:
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6)
else:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", True)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", True)
elif enable_ipv6:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")

View File

@@ -1,5 +1,4 @@
#include "pid_climate.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -163,16 +162,14 @@ void PIDClimate::start_autotune(std::unique_ptr<PIDAutotuner> &&autotune) {
float min_value = this->supports_cool_() ? -1.0f : 0.0f;
float max_value = this->supports_heat_() ? 1.0f : 0.0f;
this->autotuner_->config(min_value, max_value);
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_object_id_to(object_id_buf);
this->autotuner_->set_autotuner_id(std::string(object_id.c_str()));
this->autotuner_->set_autotuner_id(this->get_object_id());
ESP_LOGI(TAG,
"%s: Autotune has started. This can take a long time depending on the "
"responsiveness of your system. Your system "
"output will be altered to deliberately oscillate above and below the setpoint multiple times. "
"Until your sensor provides a reading, the autotuner may display \'nan\'",
object_id.c_str());
this->get_object_id().c_str());
this->set_interval("autotune-progress", 10000, [this]() {
if (this->autotuner_ != nullptr && !this->autotuner_->is_finished())
@@ -180,7 +177,8 @@ void PIDClimate::start_autotune(std::unique_ptr<PIDAutotuner> &&autotune) {
});
if (mode != climate::CLIMATE_MODE_HEAT_COOL) {
ESP_LOGW(TAG, "%s: !!! For PID autotuner you need to set AUTO (also called heat/cool) mode!", object_id.c_str());
ESP_LOGW(TAG, "%s: !!! For PID autotuner you need to set AUTO (also called heat/cool) mode!",
this->get_object_id().c_str());
}
}

View File

@@ -112,12 +112,7 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
std::string PrometheusHandler::relabel_id_(EntityBase *obj) {
auto item = relabel_map_id_.find(obj);
if (item != relabel_map_id_.end()) {
return item->second;
}
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = obj->get_object_id_to(object_id_buf);
return std::string(object_id.c_str());
return item == relabel_map_id_.end() ? obj->get_object_id() : item->second;
}
std::string PrometheusHandler::relabel_name_(EntityBase *obj) {

View File

@@ -108,6 +108,9 @@ def register_trigger(name, type, data_type):
validator = automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type),
cv.Optional(CONF_RECEIVER_ID): cv.invalid(
"This has been removed in ESPHome 2022.3.0 and the trigger attaches directly to the parent receiver."
),
}
)
registerer = TRIGGER_REGISTRY.register(f"on_{name}", validator)
@@ -204,7 +207,13 @@ validate_binary_sensor = cv.validate_registry_entry(
"remote receiver", BINARY_SENSOR_REGISTRY
)
TRIGGER_REGISTRY = SimpleRegistry()
DUMPER_REGISTRY = Registry()
DUMPER_REGISTRY = Registry(
{
cv.Optional(CONF_RECEIVER_ID): cv.invalid(
"This has been removed in ESPHome 1.20.0 and the dumper attaches directly to the parent receiver."
),
}
)
def validate_dumpers(value):
@@ -471,6 +480,10 @@ COOLIX_BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_FIRST): cv.hex_int_range(0, 16777215),
cv.Optional(CONF_SECOND, default=0): cv.hex_int_range(0, 16777215),
cv.Optional(CONF_DATA): cv.invalid(
"'data' option has been removed in ESPHome 2023.8. "
"Use the 'first' and 'second' options instead."
),
}
)

View File

@@ -141,14 +141,7 @@ uint32_t SafeModeComponent::read_rtc_() {
return val;
}
void SafeModeComponent::clean_rtc() {
// Save without sync - preferences will be written at shutdown or by IntervalSyncer.
// This avoids blocking the loop for 50+ ms on flash write. If the device crashes
// before sync, the boot wasn't really successful anyway and the counter should
// remain incremented.
uint32_t val = 0;
this->rtc_.save(&val);
}
void SafeModeComponent::clean_rtc() { this->write_rtc_(0); }
void SafeModeComponent::on_safe_shutdown() {
if (this->read_rtc_() != SafeModeComponent::ENTER_SAFE_MODE_MAGIC)

View File

@@ -77,21 +77,23 @@ class Select : public EntityBase {
void add_on_state_callback(std::function<void(size_t)> &&callback);
/** Set the value of the select by index, this is an optional virtual method.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control().
* Override this to work directly with indices and avoid string conversions.
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
protected:
friend class SelectCall;
size_t active_index_{0};
/** Set the value of the select by index, this is an optional virtual method.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
* Overriding this index-based version is PREFERRED as it avoids string conversions.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control(const std::string&).
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
/** Set the value of the select, this is a virtual method that each select integration can implement.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.

View File

@@ -304,6 +304,9 @@ _SENSOR_SCHEMA = (
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_STATE_CLASS): validate_state_class,
cv.Optional(CONF_ENTITY_CATEGORY): sensor_entity_category,
cv.Optional("last_reset_type"): cv.invalid(
"last_reset_type has been removed since 2021.9.0. state_class: total_increasing should be used for total values."
),
cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean,
cv.Optional(CONF_EXPIRE_AFTER): cv.All(
cv.requires_component("mqtt"),

View File

@@ -14,34 +14,27 @@
namespace esphome::socket {
// Format sockaddr into caller-provided buffer, returns length written (excluding null)
size_t format_sockaddr_to(const struct sockaddr_storage &storage, std::span<char, PEERNAME_MAX_LEN> buf) {
std::string format_sockaddr(const struct sockaddr_storage &storage) {
if (storage.ss_family == AF_INET) {
const struct sockaddr_in *addr = reinterpret_cast<const struct sockaddr_in *>(&storage);
if (inet_ntop(AF_INET, &addr->sin_addr, buf.data(), buf.size()) != nullptr)
return strlen(buf.data());
char buf[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)) != nullptr)
return std::string{buf};
}
#if LWIP_IPV6
else if (storage.ss_family == AF_INET6) {
const struct sockaddr_in6 *addr = reinterpret_cast<const struct sockaddr_in6 *>(&storage);
char buf[INET6_ADDRSTRLEN];
// Format IPv4-mapped IPv6 addresses as regular IPv4 addresses
if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 &&
addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) &&
inet_ntop(AF_INET, &addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) {
return strlen(buf.data());
inet_ntop(AF_INET, &addr->sin6_addr.un.u32_addr[3], buf, sizeof(buf)) != nullptr) {
return std::string{buf};
}
if (inet_ntop(AF_INET6, &addr->sin6_addr, buf.data(), buf.size()) != nullptr)
return strlen(buf.data());
if (inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)) != nullptr)
return std::string{buf};
}
#endif
buf[0] = '\0';
return 0;
}
std::string format_sockaddr(const struct sockaddr_storage &storage) {
char buf[PEERNAME_MAX_LEN];
if (format_sockaddr_to(storage, buf) > 0)
return std::string{buf};
return {};
}
@@ -107,15 +100,6 @@ class BSDSocketImpl : public Socket {
return {};
return format_sockaddr(storage);
}
size_t getpeername_to(std::span<char, PEERNAME_MAX_LEN> buf) override {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (::getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(storage, buf);
}
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
return ::getsockname(this->fd_, addr, addrlen);
}

View File

@@ -196,14 +196,6 @@ class LWIPRawImpl : public Socket {
}
return this->format_ip_address_(pcb_->remote_ip);
}
size_t getpeername_to(std::span<char, PEERNAME_MAX_LEN> buf) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
buf[0] = '\0';
return 0;
}
return this->format_ip_address_to_(pcb_->remote_ip, buf);
}
int getsockname(struct sockaddr *name, socklen_t *addrlen) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
@@ -525,27 +517,17 @@ class LWIPRawImpl : public Socket {
}
protected:
// Format IP address into caller-provided buffer, returns length written (excluding null)
size_t format_ip_address_to_(const ip_addr_t &ip, std::span<char, PEERNAME_MAX_LEN> buf) {
std::string format_ip_address_(const ip_addr_t &ip) {
char buffer[50] = {};
if (IP_IS_V4_VAL(ip)) {
inet_ntoa_r(ip, buf.data(), buf.size());
return strlen(buf.data());
inet_ntoa_r(ip, buffer, sizeof(buffer));
}
#if LWIP_IPV6
else if (IP_IS_V6_VAL(ip)) {
inet6_ntoa_r(ip, buf.data(), buf.size());
return strlen(buf.data());
inet6_ntoa_r(ip, buffer, sizeof(buffer));
}
#endif
buf[0] = '\0';
return 0;
}
std::string format_ip_address_(const ip_addr_t &ip) {
char buffer[PEERNAME_MAX_LEN];
if (format_ip_address_to_(ip, buffer) > 0)
return std::string(buffer);
return {};
return std::string(buffer);
}
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) {

View File

@@ -9,36 +9,25 @@
namespace esphome::socket {
// Format sockaddr into caller-provided buffer, returns length written (excluding null)
size_t format_sockaddr_to(const struct sockaddr_storage &storage, std::span<char, PEERNAME_MAX_LEN> buf) {
std::string format_sockaddr(const struct sockaddr_storage &storage) {
if (storage.ss_family == AF_INET) {
const struct sockaddr_in *addr = reinterpret_cast<const struct sockaddr_in *>(&storage);
const char *ret = lwip_inet_ntop(AF_INET, &addr->sin_addr, buf.data(), buf.size());
if (ret == nullptr) {
buf[0] = '\0';
return 0;
}
return strlen(buf.data());
char buf[INET_ADDRSTRLEN];
const char *ret = lwip_inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf));
if (ret == nullptr)
return {};
return std::string{buf};
}
#if LWIP_IPV6
else if (storage.ss_family == AF_INET6) {
const struct sockaddr_in6 *addr = reinterpret_cast<const struct sockaddr_in6 *>(&storage);
const char *ret = lwip_inet_ntop(AF_INET6, &addr->sin6_addr, buf.data(), buf.size());
if (ret == nullptr) {
buf[0] = '\0';
return 0;
}
return strlen(buf.data());
char buf[INET6_ADDRSTRLEN];
const char *ret = lwip_inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf));
if (ret == nullptr)
return {};
return std::string{buf};
}
#endif
buf[0] = '\0';
return 0;
}
std::string format_sockaddr(const struct sockaddr_storage &storage) {
char buf[PEERNAME_MAX_LEN];
if (format_sockaddr_to(storage, buf) > 0)
return std::string{buf};
return {};
}
@@ -106,15 +95,6 @@ class LwIPSocketImpl : public Socket {
return {};
return format_sockaddr(storage);
}
size_t getpeername_to(std::span<char, PEERNAME_MAX_LEN> buf) override {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (lwip_getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(storage, buf);
}
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
return lwip_getsockname(this->fd_, addr, addrlen);
}

View File

@@ -1,6 +1,5 @@
#pragma once
#include <memory>
#include <span>
#include <string>
#include "esphome/core/optional.h"
@@ -9,15 +8,6 @@
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
namespace esphome::socket {
// Maximum length for peer name string (IP address without port)
// IPv4: "255.255.255.255" = 15 chars + null = 16
// IPv6: full address = 45 chars + null = 46
#if LWIP_IPV6
static constexpr size_t PEERNAME_MAX_LEN = 46; // INET6_ADDRSTRLEN
#else
static constexpr size_t PEERNAME_MAX_LEN = 16; // INET_ADDRSTRLEN
#endif
class Socket {
public:
Socket() = default;
@@ -42,9 +32,6 @@ class Socket {
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual std::string getpeername() = 0;
/// Format peer address into a fixed-size buffer (no heap allocation)
/// Returns number of characters written (excluding null terminator), or 0 on error
virtual size_t getpeername_to(std::span<char, PEERNAME_MAX_LEN> buf) = 0;
virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual std::string getsockname() = 0;
virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0;

View File

@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SPEED): cv.invalid(
"Configuring individual speeds is deprecated."
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)

View File

@@ -10,7 +10,7 @@ namespace speed {
class SpeedFan : public Component, public fan::Fan {
public:
SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {}
SpeedFan(int speed_count) : speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_output(output::FloatOutput *output) { this->output_ = output; }
@@ -26,7 +26,7 @@ class SpeedFan : public Component, public fan::Fan {
output::FloatOutput *output_;
output::BinaryOutput *oscillating_{nullptr};
output::BinaryOutput *direction_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};

View File

@@ -19,7 +19,6 @@ from esphome.const import (
UNIT_MINUTE,
UNIT_SECOND,
)
from esphome.helpers import docs_url
AUTO_LOAD = ["number", "switch"]
CODEOWNERS = ["@kbx81"]
@@ -163,9 +162,55 @@ def validate_sprinkler(config):
raise cv.Invalid(
f"{CONF_RUN_DURATION} must be greater than {CONF_VALVE_OPEN_DELAY}"
)
if CONF_VALVE_SWITCH_ID not in valve:
if (
CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID not in valve
) or (
CONF_PUMP_ON_SWITCH_ID in valve and CONF_PUMP_OFF_SWITCH_ID not in valve
):
raise cv.Invalid(
f"{CONF_VALVE_SWITCH_ID} must be specified in valve configuration"
f"Both {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID} must be specified for latching pump configuration"
)
if CONF_PUMP_SWITCH_ID in valve and (
CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve
):
raise cv.Invalid(
f"Do not specify {CONF_PUMP_OFF_SWITCH_ID} or {CONF_PUMP_ON_SWITCH_ID} when using {CONF_PUMP_SWITCH_ID}"
)
if CONF_PUMP_PULSE_DURATION not in sprinkler_controller and (
CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve
):
raise cv.Invalid(
f"{CONF_PUMP_PULSE_DURATION} must be specified when using {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID}"
)
if (
CONF_VALVE_OFF_SWITCH_ID in valve
and CONF_VALVE_ON_SWITCH_ID not in valve
) or (
CONF_VALVE_ON_SWITCH_ID in valve
and CONF_VALVE_OFF_SWITCH_ID not in valve
):
raise cv.Invalid(
f"Both {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified for latching valve configuration"
)
if CONF_VALVE_SWITCH_ID in valve and (
CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve
):
raise cv.Invalid(
f"Do not specify {CONF_VALVE_OFF_SWITCH_ID} or {CONF_VALVE_ON_SWITCH_ID} when using {CONF_VALVE_SWITCH_ID}"
)
if CONF_VALVE_PULSE_DURATION not in sprinkler_controller and (
CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve
):
raise cv.Invalid(
f"{CONF_VALVE_PULSE_DURATION} must be specified when using {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID}"
)
if (
CONF_VALVE_SWITCH_ID not in valve
and CONF_VALVE_OFF_SWITCH_ID not in valve
and CONF_VALVE_ON_SWITCH_ID not in valve
):
raise cv.Invalid(
f"Either {CONF_VALVE_SWITCH_ID} or {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified in valve configuration"
)
if CONF_RUN_DURATION not in valve and CONF_RUN_DURATION_NUMBER not in valve:
raise cv.Invalid(
@@ -245,15 +290,8 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema(
),
key=CONF_NAME,
),
# Removed latching pump keys - accepted for validation error reporting
cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.invalid(
f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. "
f"See {docs_url('components/switch/h_bridge')} for more information"
),
cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.invalid(
f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. "
f"See {docs_url('components/switch/h_bridge')} for more information"
),
cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.use_id(switch.Switch),
cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.use_id(switch.Switch),
cv.Optional(CONF_PUMP_SWITCH_ID): cv.use_id(switch.Switch),
cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_seconds,
cv.Optional(CONF_RUN_DURATION_NUMBER): cv.maybe_simple_value(
@@ -283,15 +321,8 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema(
switch.switch_schema(SprinklerControllerSwitch),
key=CONF_NAME,
),
# Removed latching valve keys - accepted for validation error reporting
cv.Optional(CONF_VALVE_OFF_SWITCH_ID): cv.invalid(
f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. "
f"See {docs_url('components/switch/h_bridge')} for more information"
),
cv.Optional(CONF_VALVE_ON_SWITCH_ID): cv.invalid(
f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. "
f"See {docs_url('components/switch/h_bridge')} for more information"
),
cv.Optional(CONF_VALVE_OFF_SWITCH_ID): cv.use_id(switch.Switch),
cv.Optional(CONF_VALVE_ON_SWITCH_ID): cv.use_id(switch.Switch),
cv.Optional(CONF_VALVE_SWITCH_ID): cv.use_id(switch.Switch),
}
)
@@ -379,15 +410,8 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema(
validate_min_max,
key=CONF_NAME,
),
# Removed latching valve keys - accepted for validation error reporting
cv.Optional(CONF_PUMP_PULSE_DURATION): cv.invalid(
f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. "
f"See {docs_url('components/switch/h_bridge')} for more information"
),
cv.Optional(CONF_VALVE_PULSE_DURATION): cv.invalid(
f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. "
f"See {docs_url('components/switch/h_bridge')} for more information"
),
cv.Optional(CONF_PUMP_PULSE_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_VALVE_PULSE_DURATION): cv.positive_time_period_milliseconds,
cv.Exclusive(
CONF_PUMP_START_PUMP_DELAY, "pump_start_xxxx_delay"
): cv.positive_time_period_seconds,
@@ -741,10 +765,35 @@ async def to_code(config):
valve_index, valve_switch, valve[CONF_RUN_DURATION]
)
)
elif CONF_VALVE_OFF_SWITCH_ID in valve and CONF_VALVE_ON_SWITCH_ID in valve:
valve_switch_off = await cg.get_variable(
valve[CONF_VALVE_OFF_SWITCH_ID]
)
valve_switch_on = await cg.get_variable(valve[CONF_VALVE_ON_SWITCH_ID])
cg.add(
var.configure_valve_switch_pulsed(
valve_index,
valve_switch_off,
valve_switch_on,
sprinkler_controller[CONF_VALVE_PULSE_DURATION],
valve[CONF_RUN_DURATION],
)
)
if CONF_PUMP_SWITCH_ID in valve:
pump = await cg.get_variable(valve[CONF_PUMP_SWITCH_ID])
cg.add(var.configure_valve_pump_switch(valve_index, pump))
elif CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID in valve:
pump_off = await cg.get_variable(valve[CONF_PUMP_OFF_SWITCH_ID])
pump_on = await cg.get_variable(valve[CONF_PUMP_ON_SWITCH_ID])
cg.add(
var.configure_valve_pump_switch_pulsed(
valve_index,
pump_off,
pump_on,
sprinkler_controller[CONF_PUMP_PULSE_DURATION],
)
)
if CONF_RUN_DURATION_NUMBER in valve:
num_rd_var = await number.new_number(

View File

@@ -11,6 +11,70 @@ namespace esphome::sprinkler {
static const char *const TAG = "sprinkler";
SprinklerSwitch::SprinklerSwitch() {}
SprinklerSwitch::SprinklerSwitch(switch_::Switch *sprinkler_switch) : on_switch_(sprinkler_switch) {}
SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration)
: pulse_duration_(pulse_duration), off_switch_(off_switch), on_switch_(on_switch) {}
bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); }
void SprinklerSwitch::loop() {
if ((this->pinned_millis_) && (App.get_loop_component_start_time() > this->pinned_millis_ + this->pulse_duration_)) {
this->pinned_millis_ = 0; // reset tracker
if (this->off_switch_->state) {
this->off_switch_->turn_off();
}
if (this->on_switch_->state) {
this->on_switch_->turn_off();
}
}
}
void SprinklerSwitch::turn_off() {
if (!this->state()) { // do nothing if we're already in the requested state
return;
}
if (this->off_switch_ != nullptr) { // latching valve, start a pulse
if (!this->off_switch_->state) {
this->off_switch_->turn_on();
}
this->pinned_millis_ = millis();
} else if (this->on_switch_ != nullptr) { // non-latching valve
this->on_switch_->turn_off();
}
this->state_ = false;
}
void SprinklerSwitch::turn_on() {
if (this->state()) { // do nothing if we're already in the requested state
return;
}
if (this->off_switch_ != nullptr) { // latching valve, start a pulse
if (!this->on_switch_->state) {
this->on_switch_->turn_on();
}
this->pinned_millis_ = millis();
} else if (this->on_switch_ != nullptr) { // non-latching valve
this->on_switch_->turn_on();
}
this->state_ = true;
}
bool SprinklerSwitch::state() {
if ((this->off_switch_ == nullptr) && (this->on_switch_ != nullptr)) { // latching valve is not configured...
return this->on_switch_->state; // ...so just return the pump switch state
}
return this->state_;
}
void SprinklerSwitch::sync_valve_state(bool latch_state) {
if (this->is_latching_valve()) {
this->state_ = latch_state;
} else if (this->on_switch_ != nullptr) {
this->state_ = this->on_switch_->state;
}
}
void SprinklerControllerNumber::setup() {
float value;
if (!this->restore_value_) {
@@ -155,8 +219,8 @@ void SprinklerValveOperator::start() {
this->state_ = STARTING; // STARTING state requires both a pump and a start_delay_
if (this->start_delay_is_valve_delay_) {
this->pump_on_();
} else if (!this->pump_switch()->state) { // if the pump is already on, wait to switch on the valve
this->valve_on_(); // to ensure consistent run time
} else if (!this->pump_switch()->state()) { // if the pump is already on, wait to switch on the valve
this->valve_on_(); // to ensure consistent run time
}
} else {
this->run_(); // there is no start_delay_, so just start the pump and valve
@@ -176,8 +240,8 @@ void SprinklerValveOperator::stop() {
} else {
this->valve_off_();
}
if (this->pump_switch()->state) { // if the pump is still on at this point, it may be in use...
this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time
if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use...
this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time
}
} else {
this->kill_(); // there is no stop_delay_, so just stop the pump and valve
@@ -210,7 +274,7 @@ uint32_t SprinklerValveOperator::time_remaining() {
SprinklerState SprinklerValveOperator::state() { return this->state_; }
switch_::Switch *SprinklerValveOperator::pump_switch() {
SprinklerSwitch *SprinklerValveOperator::pump_switch() {
if ((this->controller_ == nullptr) || (this->valve_ == nullptr)) {
return nullptr;
}
@@ -221,50 +285,48 @@ switch_::Switch *SprinklerValveOperator::pump_switch() {
}
void SprinklerValveOperator::pump_off_() {
auto *pump = this->pump_switch();
if ((this->valve_ == nullptr) || (pump == nullptr)) { // safety first!
if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first!
return;
}
if (this->controller_ == nullptr) { // safety first!
pump->turn_off(); // if no controller was set, just switch off the pump
this->pump_switch()->turn_off(); // if no controller was set, just switch off the pump
} else { // ...otherwise, do it "safely"
auto state = this->state_; // this is silly, but...
this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does
this->controller_->set_pump_state(pump, false);
this->controller_->set_pump_state(this->pump_switch(), false);
this->state_ = state;
}
}
void SprinklerValveOperator::pump_on_() {
auto *pump = this->pump_switch();
if ((this->valve_ == nullptr) || (pump == nullptr)) { // safety first!
if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first!
return;
}
if (this->controller_ == nullptr) { // safety first!
pump->turn_on(); // if no controller was set, just switch on the pump
this->pump_switch()->turn_on(); // if no controller was set, just switch on the pump
} else { // ...otherwise, do it "safely"
auto state = this->state_; // this is silly, but...
this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does
this->controller_->set_pump_state(pump, true);
this->controller_->set_pump_state(this->pump_switch(), true);
this->state_ = state;
}
}
void SprinklerValveOperator::valve_off_() {
if ((this->valve_ == nullptr) || (this->valve_->valve_switch == nullptr)) { // safety first!
if (this->valve_ == nullptr) { // safety first!
return;
}
if (this->valve_->valve_switch->state) {
this->valve_->valve_switch->turn_off();
if (this->valve_->valve_switch.state()) {
this->valve_->valve_switch.turn_off();
}
}
void SprinklerValveOperator::valve_on_() {
if ((this->valve_ == nullptr) || (this->valve_->valve_switch == nullptr)) { // safety first!
if (this->valve_ == nullptr) { // safety first!
return;
}
if (!this->valve_->valve_switch->state) {
this->valve_->valve_switch->turn_on();
if (!this->valve_->valve_switch.state()) {
this->valve_->valve_switch.turn_on();
}
}
@@ -339,6 +401,12 @@ Sprinkler::Sprinkler(const std::string &name) {
void Sprinkler::setup() { this->all_valves_off_(true); }
void Sprinkler::loop() {
for (auto &p : this->pump_) {
p.loop();
}
for (auto &v : this->valve_) {
v.valve_switch.loop();
}
for (auto &vo : this->valve_op_) {
vo.loop();
}
@@ -355,15 +423,10 @@ void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControll
new_valve->controller_switch = valve_sw;
new_valve->controller_switch->set_state_lambda([this, new_valve_number]() -> optional<bool> {
auto *valve = this->valve_switch(new_valve_number);
auto *pump = this->valve_pump_switch(new_valve_number);
if (valve == nullptr) {
return false;
if (this->valve_pump_switch(new_valve_number) != nullptr) {
return this->valve_switch(new_valve_number)->state() && this->valve_pump_switch(new_valve_number)->state();
}
if (pump != nullptr) {
return valve->state && pump->state;
}
return valve->state;
return this->valve_switch(new_valve_number)->state();
});
new_valve->valve_turn_off_automation =
@@ -433,7 +496,18 @@ void Sprinkler::set_controller_repeat_number(SprinklerControllerNumber *repeat_n
void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) {
if (this->is_a_valid_valve(valve_number)) {
this->valve_[valve_number].valve_switch = valve_switch;
this->valve_[valve_number].valve_switch.set_on_switch(valve_switch);
this->valve_[valve_number].run_duration = run_duration;
}
}
void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off,
switch_::Switch *valve_switch_on, uint32_t pulse_duration,
uint32_t run_duration) {
if (this->is_a_valid_valve(valve_number)) {
this->valve_[valve_number].valve_switch.set_off_switch(valve_switch_off);
this->valve_[valve_number].valve_switch.set_on_switch(valve_switch_on);
this->valve_[valve_number].valve_switch.set_pulse_duration(pulse_duration);
this->valve_[valve_number].run_duration = run_duration;
}
}
@@ -441,12 +515,31 @@ void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *val
void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch) {
if (this->is_a_valid_valve(valve_number)) {
for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump
if (this->pump_[i] == pump_switch) { // if the "new" pump matches one we already have...
this->valve_[valve_number].pump_switch_index = i; // ...save its index in the pump vector...
if (this->pump_[i].on_switch() == pump_switch) { // if the "new" pump matches one we already have...
this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_...
return; // ...and we are done
}
} // if we end up here, no pumps matched, so add a new one
this->pump_.push_back(pump_switch);
} // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it
this->pump_.resize(this->pump_.size() + 1);
this->pump_.back().set_on_switch(pump_switch);
this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump
}
}
void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off,
switch_::Switch *pump_switch_on, uint32_t pulse_duration) {
if (this->is_a_valid_valve(valve_number)) {
for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump
if ((this->pump_[i].off_switch() == pump_switch_off) &&
(this->pump_[i].on_switch() == pump_switch_on)) { // if the "new" pump matches one we already have...
this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_...
return; // ...and we are done
}
} // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it
this->pump_.resize(this->pump_.size() + 1);
this->pump_.back().set_off_switch(pump_switch_off);
this->pump_.back().set_on_switch(pump_switch_on);
this->pump_.back().set_pulse_duration(pulse_duration);
this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump
}
}
@@ -948,7 +1041,7 @@ size_t Sprinkler::number_of_valves() { return this->valve_.size(); }
bool Sprinkler::is_a_valid_valve(const size_t valve_number) { return (valve_number < this->number_of_valves()); }
bool Sprinkler::pump_in_use(switch_::Switch *pump_switch) {
bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) {
if (pump_switch == nullptr) {
return false; // we can't do anything if there's nothing to check
}
@@ -961,7 +1054,8 @@ bool Sprinkler::pump_in_use(switch_::Switch *pump_switch) {
for (auto &vo : this->valve_op_) { // first, check if any SprinklerValveOperator has a valve dependent on this pump
if ((vo.state() != BYPASS) && (vo.pump_switch() != nullptr)) {
// the SprinklerValveOperator is configured with a pump; now check if it is the pump of interest
if (vo.pump_switch() == pump_switch) {
if ((vo.pump_switch()->off_switch() == pump_switch->off_switch()) &&
(vo.pump_switch()->on_switch() == pump_switch->on_switch())) {
// now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or
// is STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now
if ((vo.state() == ACTIVE) ||
@@ -980,12 +1074,13 @@ bool Sprinkler::pump_in_use(switch_::Switch *pump_switch) {
if (valve_pump == nullptr) {
return false; // valve has no pump, so this pump isn't in use by it
}
return pump_switch == valve_pump;
return (pump_switch->off_switch() == valve_pump->off_switch()) &&
(pump_switch->on_switch() == valve_pump->on_switch());
}
return false;
}
void Sprinkler::set_pump_state(switch_::Switch *pump_switch, bool state) {
void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) {
if (pump_switch == nullptr) {
return; // we can't do anything if there's nothing to check
}
@@ -996,10 +1091,15 @@ void Sprinkler::set_pump_state(switch_::Switch *pump_switch, bool state) {
if (controller != this) { // dummy check
if (controller->pump_in_use(pump_switch)) {
hold_pump_on = true; // if another controller says it's using this pump, keep it on
// at this point we know if there exists another SprinklerSwitch that is "on" with its
// off_switch_ and on_switch_ pointers pointing to the same pair of switch objects
}
}
}
if (hold_pump_on) {
// at this point we know if there exists another SprinklerSwitch that is "on" with its
// off_switch_ and on_switch_ pointers pointing to the same pair of switch objects...
pump_switch->sync_valve_state(true); // ...so ensure our state is consistent
ESP_LOGD(TAG, "Leaving pump on because another controller instance is using it");
}
@@ -1007,6 +1107,8 @@ void Sprinkler::set_pump_state(switch_::Switch *pump_switch, bool state) {
pump_switch->turn_on();
} else if (!hold_pump_on && !this->pump_in_use(pump_switch)) {
pump_switch->turn_off();
} else if (hold_pump_on) { // we must assume the other controller will switch off the pump when done...
pump_switch->sync_valve_state(false); // ...this only impacts latching valves
}
}
@@ -1172,23 +1274,23 @@ SprinklerControllerSwitch *Sprinkler::enable_switch(size_t valve_number) {
return nullptr;
}
switch_::Switch *Sprinkler::valve_switch(const size_t valve_number) {
SprinklerSwitch *Sprinkler::valve_switch(const size_t valve_number) {
if (this->is_a_valid_valve(valve_number)) {
return this->valve_[valve_number].valve_switch;
return &this->valve_[valve_number].valve_switch;
}
return nullptr;
}
switch_::Switch *Sprinkler::valve_pump_switch(const size_t valve_number) {
SprinklerSwitch *Sprinkler::valve_pump_switch(const size_t valve_number) {
if (this->is_a_valid_valve(valve_number) && this->valve_[valve_number].pump_switch_index.has_value()) {
return this->pump_[this->valve_[valve_number].pump_switch_index.value()];
return &this->pump_[this->valve_[valve_number].pump_switch_index.value()];
}
return nullptr;
}
switch_::Switch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) {
SprinklerSwitch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) {
if (pump_index < this->pump_.size()) {
return this->pump_[pump_index];
return &this->pump_[pump_index];
}
return nullptr;
}
@@ -1352,9 +1454,8 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) {
void Sprinkler::all_valves_off_(const bool include_pump) {
for (size_t valve_index = 0; valve_index < this->number_of_valves(); valve_index++) {
auto *valve_sw = this->valve_[valve_index].valve_switch;
if ((valve_sw != nullptr) && valve_sw->state) {
valve_sw->turn_off();
if (this->valve_[valve_index].valve_switch.state()) {
this->valve_[valve_index].valve_switch.turn_off();
}
if (include_pump) {
this->set_pump_state(this->valve_pump_switch(valve_index), false);
@@ -1653,6 +1754,10 @@ void Sprinkler::dump_config() {
" Name: %s\n"
" Run Duration: %" PRIu32 " seconds",
valve_number, this->valve_name(valve_number), this->valve_run_duration(valve_number));
if (this->valve_[valve_number].valve_switch.pulse_duration()) {
ESP_LOGCONFIG(TAG, " Pulse Duration: %" PRIu32 " milliseconds",
this->valve_[valve_number].valve_switch.pulse_duration());
}
}
if (!this->pump_.empty()) {
ESP_LOGCONFIG(TAG, " Total number of pumps: %zu", this->pump_.size());

View File

@@ -35,6 +35,7 @@ enum SprinklerValveRunRequestOrigin : uint8_t {
class Sprinkler; // this component
class SprinklerControllerNumber; // number components that appear in the front end; based on number core
class SprinklerControllerSwitch; // switches that appear in the front end; based on switch core
class SprinklerSwitch; // switches representing any valve or pump; provides abstraction for latching valves
class SprinklerValveOperator; // manages all switching on/off of valves and associated pumps
class SprinklerValveRunRequest; // tells the sprinkler controller what valve to run and for how long as well as what
// SprinklerValveOperator is handling it
@@ -42,6 +43,34 @@ template<typename... Ts> class StartSingleValveAction;
template<typename... Ts> class ShutdownAction;
template<typename... Ts> class ResumeOrStartAction;
class SprinklerSwitch {
public:
SprinklerSwitch();
SprinklerSwitch(switch_::Switch *sprinkler_switch);
SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration);
bool is_latching_valve(); // returns true if configured as a latching valve
void loop(); // called as a part of loop(), used for latching valve pulses
uint32_t pulse_duration() { return this->pulse_duration_; }
bool state(); // returns the switch's current state
void set_off_switch(switch_::Switch *off_switch) { this->off_switch_ = off_switch; }
void set_on_switch(switch_::Switch *on_switch) { this->on_switch_ = on_switch; }
void set_pulse_duration(uint32_t pulse_duration) { this->pulse_duration_ = pulse_duration; }
void sync_valve_state(
bool latch_state); // syncs internal state to switch; if latching valve, sets state to latch_state
void turn_off(); // sets internal flag and actuates the switch
void turn_on(); // sets internal flag and actuates the switch
switch_::Switch *off_switch() { return this->off_switch_; }
switch_::Switch *on_switch() { return this->on_switch_; }
protected:
bool state_{false};
uint32_t pulse_duration_{0};
uint64_t pinned_millis_{0};
switch_::Switch *off_switch_{nullptr}; // only used for latching valves
switch_::Switch *on_switch_{nullptr}; // used for both latching and non-latching valves
};
struct SprinklerQueueItem {
size_t valve_number;
uint32_t run_duration;
@@ -59,7 +88,7 @@ struct SprinklerValve {
SprinklerControllerNumber *run_duration_number;
SprinklerControllerSwitch *controller_switch;
SprinklerControllerSwitch *enable_switch;
switch_::Switch *valve_switch;
SprinklerSwitch valve_switch;
uint32_t run_duration;
optional<size_t> pump_switch_index;
bool valve_cycle_complete;
@@ -126,7 +155,7 @@ class SprinklerValveOperator {
uint32_t run_duration(); // returns the desired run duration in seconds
uint32_t time_remaining(); // returns seconds remaining (does not include stop_delay_)
SprinklerState state(); // returns the valve's state/status
switch_::Switch *pump_switch(); // returns this SprinklerValveOperator's pump switch
SprinklerSwitch *pump_switch(); // returns this SprinklerValveOperator's pump's SprinklerSwitch
protected:
void pump_off_();
@@ -199,9 +228,13 @@ class Sprinkler : public Component {
/// configure a valve's switch object and run duration. run_duration is time in seconds.
void configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration);
void configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off,
switch_::Switch *valve_switch_on, uint32_t pulse_duration, uint32_t run_duration);
/// configure a valve's associated pump switch object
void configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch);
void configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off,
switch_::Switch *pump_switch_on, uint32_t pulse_duration);
/// configure a valve's run duration number component
void configure_valve_run_duration_number(size_t valve_number, SprinklerControllerNumber *run_duration_number);
@@ -350,10 +383,10 @@ class Sprinkler : public Component {
bool is_a_valid_valve(size_t valve_number);
/// returns true if the pump the pointer points to is in use
bool pump_in_use(switch_::Switch *pump_switch);
bool pump_in_use(SprinklerSwitch *pump_switch);
/// switches on/off a pump "safely" by checking that the new state will not conflict with another controller
void set_pump_state(switch_::Switch *pump_switch, bool state);
void set_pump_state(SprinklerSwitch *pump_switch, bool state);
/// returns the amount of time in seconds required for all valves
uint32_t total_cycle_time_all_valves();
@@ -386,13 +419,13 @@ class Sprinkler : public Component {
SprinklerControllerSwitch *enable_switch(size_t valve_number);
/// returns a pointer to a valve's switch object
switch_::Switch *valve_switch(size_t valve_number);
SprinklerSwitch *valve_switch(size_t valve_number);
/// returns a pointer to a valve's pump switch object
switch_::Switch *valve_pump_switch(size_t valve_number);
SprinklerSwitch *valve_pump_switch(size_t valve_number);
/// returns a pointer to a valve's pump switch object
switch_::Switch *valve_pump_switch_by_pump_index(size_t pump_index);
SprinklerSwitch *valve_pump_switch_by_pump_index(size_t pump_index);
protected:
/// returns true if valve number is enabled
@@ -544,8 +577,8 @@ class Sprinkler : public Component {
/// Queue of valves to activate next, regardless of auto-advance
std::vector<SprinklerQueueItem> queued_valves_;
/// Sprinkler valve pump switches
std::vector<switch_::Switch *> pump_;
/// Sprinkler valve pump objects
std::vector<SprinklerSwitch> pump_;
/// Sprinkler valve objects
std::vector<SprinklerValve> valve_;

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID
from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID, CONF_SCAN
CODEOWNERS = ["@andreashergert1984"]
@@ -18,6 +18,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(TCA9548AComponent),
cv.Optional(CONF_SCAN): cv.invalid("This option has been removed"),
cv.Optional(CONF_CHANNELS, default=[]): cv.ensure_list(
{
cv.Required(CONF_BUS_ID): cv.declare_id(TCA9548AChannel),

View File

@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
{
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)

View File

@@ -12,7 +12,7 @@ class TemplateFan final : public Component, public fan::Fan {
void dump_config() override;
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
void set_speed_count(uint8_t count) { this->speed_count_ = count; }
void set_speed_count(int count) { this->speed_count_ = count; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; }
@@ -21,7 +21,7 @@ class TemplateFan final : public Component, public fan::Fan {
bool has_oscillating_{false};
bool has_direction_{false};
uint8_t speed_count_{0};
int speed_count_{0};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};

View File

@@ -7,6 +7,7 @@ from esphome.const import (
CONF_ID,
CONF_LAMBDA,
CONF_OPTIMISTIC,
CONF_RESTORE_STATE,
CONF_STATE,
CONF_TURN_OFF_ACTION,
CONF_TURN_ON_ACTION,
@@ -43,6 +44,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation(
single=True
),
cv.Optional(CONF_RESTORE_STATE): cv.invalid(
"The restore_state option has been removed in 2023.7.0. Use the restore_mode option instead"
),
}
)
.extend(cv.COMPONENT_SCHEMA),

View File

@@ -3,7 +3,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::thermostat {
namespace esphome {
namespace thermostat {
static const char *const TAG = "thermostat.climate";
@@ -65,12 +66,10 @@ void ThermostatClimate::setup() {
}
void ThermostatClimate::loop() {
uint32_t now = App.get_loop_component_start_time();
for (uint8_t i = 0; i < THERMOSTAT_TIMER_COUNT; i++) {
auto &timer = this->timer_[i];
if (timer.active && (now - timer.started >= timer.time)) {
for (auto &timer : this->timer_) {
if (timer.active && (timer.started + timer.time < App.get_loop_component_start_time())) {
timer.active = false;
this->call_timer_callback_(static_cast<ThermostatClimateTimerIndex>(i));
timer.func();
}
}
}
@@ -917,42 +916,8 @@ uint32_t ThermostatClimate::timer_duration_(ThermostatClimateTimerIndex timer_in
return this->timer_[timer_index].time;
}
void ThermostatClimate::call_timer_callback_(ThermostatClimateTimerIndex timer_index) {
switch (timer_index) {
case THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME:
this->cooling_max_run_time_timer_callback_();
break;
case THERMOSTAT_TIMER_COOLING_OFF:
this->cooling_off_timer_callback_();
break;
case THERMOSTAT_TIMER_COOLING_ON:
this->cooling_on_timer_callback_();
break;
case THERMOSTAT_TIMER_FAN_MODE:
this->fan_mode_timer_callback_();
break;
case THERMOSTAT_TIMER_FANNING_OFF:
this->fanning_off_timer_callback_();
break;
case THERMOSTAT_TIMER_FANNING_ON:
this->fanning_on_timer_callback_();
break;
case THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME:
this->heating_max_run_time_timer_callback_();
break;
case THERMOSTAT_TIMER_HEATING_OFF:
this->heating_off_timer_callback_();
break;
case THERMOSTAT_TIMER_HEATING_ON:
this->heating_on_timer_callback_();
break;
case THERMOSTAT_TIMER_IDLE_ON:
this->idle_on_timer_callback_();
break;
case THERMOSTAT_TIMER_COUNT:
default:
break;
}
std::function<void()> ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex timer_index) {
return this->timer_[timer_index].func;
}
void ThermostatClimate::cooling_max_run_time_timer_callback_() {
@@ -1365,64 +1330,45 @@ void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadba
void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; }
void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; }
void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; }
void ThermostatClimate::set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time) {
uint32_t new_duration_ms = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
if (this->timer_[timer_index].active) {
// Timer is running, calculate elapsed time and adjust if needed
uint32_t current_time = App.get_loop_component_start_time();
uint32_t elapsed = current_time - this->timer_[timer_index].started;
if (elapsed >= new_duration_ms) {
// Timer should complete immediately (including when new_duration_ms is 0)
ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %d >= new %d)", timer_index, elapsed, new_duration_ms);
this->timer_[timer_index].active = false;
// Trigger the timer callback immediately
this->call_timer_callback_(timer_index);
return;
} else {
// Adjust timer to run for remaining time - keep original start time
ESP_LOGVV(TAG, "timer %d adjusted: elapsed %d, new total %d, remaining %d", timer_index, elapsed, new_duration_ms,
new_duration_ms - elapsed);
this->timer_[timer_index].time = new_duration_ms;
return;
}
}
// Original logic for non-running timers
this->timer_[timer_index].time = new_duration_ms;
}
void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME, time);
this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_OFF, time);
this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_OFF].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_ON, time);
this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_ON].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FAN_MODE, time);
this->timer_[thermostat::THERMOSTAT_TIMER_FAN_MODE].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FANNING_OFF, time);
this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_OFF].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FANNING_ON, time);
this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_ON].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME, time);
this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_OFF, time);
this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_OFF].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_ON, time);
this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_ON].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) {
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_IDLE_ON, time);
this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
}
void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) {
@@ -1707,4 +1653,5 @@ ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float defau
float default_temperature_high)
: default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {}
} // namespace esphome::thermostat
} // namespace thermostat
} // namespace esphome

View File

@@ -10,7 +10,8 @@
#include <array>
#include <cinttypes>
namespace esphome::thermostat {
namespace esphome {
namespace thermostat {
enum HumidificationAction : uint8_t {
THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF = 0,
@@ -40,11 +41,13 @@ enum OnBootRestoreFrom : uint8_t {
struct ThermostatClimateTimer {
ThermostatClimateTimer() = default;
ThermostatClimateTimer(bool active, uint32_t time, uint32_t started) : active(active), time(time), started(started) {}
ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function<void()> func)
: active(active), time(time), started(started), func(std::move(func)) {}
bool active;
uint32_t time;
uint32_t started;
std::function<void()> func;
};
struct ThermostatClimateTargetTempConfig {
@@ -263,10 +266,7 @@ class ThermostatClimate : public climate::Climate, public Component {
bool cancel_timer_(ThermostatClimateTimerIndex timer_index);
bool timer_active_(ThermostatClimateTimerIndex timer_index);
uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index);
/// Call the appropriate timer callback based on timer index
void call_timer_callback_(ThermostatClimateTimerIndex timer_index);
/// Enhanced timer duration setter with running timer adjustment
void set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time);
std::function<void()> timer_cbf_(ThermostatClimateTimerIndex timer_index);
/// set_timeout() callbacks for various actions (see above)
void cooling_max_run_time_timer_callback_();
@@ -532,16 +532,27 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *prev_humidity_control_trigger_{nullptr};
/// Climate action timers
std::array<ThermostatClimateTimer, THERMOSTAT_TIMER_COUNT> timer_{};
std::array<ThermostatClimateTimer, THERMOSTAT_TIMER_COUNT> timer_{
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)),
};
/// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc)
FixedVector<PresetEntry> preset_config_{};
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
FixedVector<CustomPresetEntry> custom_preset_config_{};
private:
/// Default custom preset to use on start up (pointer to entry in custom_preset_config_)
private:
const char *default_custom_preset_{nullptr};
};
} // namespace esphome::thermostat
} // namespace thermostat
} // namespace esphome

View File

@@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
}
)
.extend(cv.COMPONENT_SCHEMA),

View File

@@ -9,7 +9,7 @@ namespace tuya {
class TuyaFan : public Component, public fan::Fan {
public:
TuyaFan(Tuya *parent, uint8_t speed_count) : parent_(parent), speed_count_(speed_count) {}
TuyaFan(Tuya *parent, int speed_count) : parent_(parent), speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
@@ -27,7 +27,7 @@ class TuyaFan : public Component, public fan::Fan {
optional<uint8_t> switch_id_{};
optional<uint8_t> oscillation_id_{};
optional<uint8_t> direction_id_{};
uint8_t speed_count_{};
int speed_count_{};
TuyaDatapointType speed_type_{};
TuyaDatapointType oscillation_type_{};
};

View File

@@ -37,6 +37,10 @@ COLOR_TYPES = {
TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component)
COLOR_CONFIG_ERROR = (
"This option has been removed, use color_datapoint and color_type instead."
)
CONFIG_SCHEMA = cv.All(
light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
{
@@ -45,6 +49,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_RGB_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR),
cv.Optional(CONF_HSV_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR),
cv.Inclusive(CONF_COLOR_DATAPOINT, "color"): cv.uint8_t,
cv.Inclusive(CONF_COLOR_TYPE, "color"): cv.enum(COLOR_TYPES, upper=True),
cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean,

View File

@@ -19,6 +19,7 @@ from esphome.const import (
CONF_DUMMY_RECEIVER_ID,
CONF_FLOW_CONTROL_PIN,
CONF_ID,
CONF_INVERT,
CONF_LAMBDA,
CONF_NUMBER,
CONF_PORT,
@@ -303,6 +304,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_PARITY, default="NONE"): cv.enum(
UART_PARITY_OPTIONS, upper=True
),
cv.Optional(CONF_INVERT): cv.invalid(
"This option has been removed. Please instead use invert in the tx/rx pin schemas."
),
cv.Optional(CONF_DEBUG): maybe_empty_debug,
}
).extend(cv.COMPONENT_SCHEMA),

View File

@@ -1,90 +0,0 @@
import esphome.codegen as cg
from esphome.components import event, uart
import esphome.config_validation as cv
from esphome.const import CONF_EVENT_TYPES, CONF_ID
from esphome.core import ID
from esphome.types import ConfigType
from .. import uart_ns
CODEOWNERS = ["@eoasmxd"]
DEPENDENCIES = ["uart"]
UARTEvent = uart_ns.class_("UARTEvent", event.Event, uart.UARTDevice, cg.Component)
def validate_event_types(value) -> list[tuple[str, str | list[int]]]:
if not isinstance(value, list):
raise cv.Invalid("Event type must be a list of key-value mappings.")
processed: list[tuple[str, str | list[int]]] = []
for item in value:
if not isinstance(item, dict):
raise cv.Invalid(f"Event type item must be a mapping (dictionary): {item}")
if len(item) != 1:
raise cv.Invalid(
f"Event type item must be a single key-value mapping: {item}"
)
# Get the single key-value pair
event_name, match_data = next(iter(item.items()))
if not isinstance(event_name, str):
raise cv.Invalid(f"Event name (key) must be a string: {event_name}")
try:
# Try to validate as list of hex bytes
match_data_bin = cv.ensure_list(cv.hex_uint8_t)(match_data)
processed.append((event_name, match_data_bin))
continue
except cv.Invalid:
pass # Not binary, try string
try:
# Try to validate as string
match_data_str = cv.string_strict(match_data)
processed.append((event_name, match_data_str))
continue
except cv.Invalid:
pass # Not string either
# If neither validation passed
raise cv.Invalid(
f"Event match data for '{event_name}' must be a string or a list of hex bytes. Invalid data: {match_data}"
)
if not processed:
raise cv.Invalid("event_types must contain at least one event mapping.")
return processed
CONFIG_SCHEMA = (
event.event_schema(UARTEvent)
.extend(
{
cv.Required(CONF_EVENT_TYPES): validate_event_types,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
event_names = [item[0] for item in config[CONF_EVENT_TYPES]]
var = await event.new_event(config, event_types=event_names)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for i, (event_name, match_data) in enumerate(config[CONF_EVENT_TYPES]):
if isinstance(match_data, str):
match_data = [ord(c) for c in match_data]
match_data_var_id = ID(
f"match_data_{config[CONF_ID]}_{i}", is_declaration=True, type=cg.uint8
)
match_data_var = cg.static_const_array(
match_data_var_id, cg.ArrayInitializer(*match_data)
)
cg.add(var.add_event_matcher(event_name, match_data_var, len(match_data)))

View File

@@ -1,48 +0,0 @@
#include "uart_event.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::uart {
static const char *const TAG = "uart.event";
void UARTEvent::setup() {}
void UARTEvent::dump_config() { LOG_EVENT("", "UART Event", this); }
void UARTEvent::loop() { this->read_data_(); }
void UARTEvent::add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len) {
this->matchers_.push_back({event_name, match_data, match_data_len});
if (match_data_len > this->max_matcher_len_) {
this->max_matcher_len_ = match_data_len;
}
}
void UARTEvent::read_data_() {
while (this->available()) {
uint8_t data;
this->read_byte(&data);
this->buffer_.push_back(data);
bool match_found = false;
for (const auto &matcher : this->matchers_) {
if (this->buffer_.size() < matcher.data_len) {
continue;
}
if (std::equal(matcher.data, matcher.data + matcher.data_len, this->buffer_.end() - matcher.data_len)) {
this->trigger(matcher.event_name);
this->buffer_.clear();
match_found = true;
break;
}
}
if (!match_found && this->max_matcher_len_ > 0 && this->buffer_.size() > this->max_matcher_len_) {
this->buffer_.erase(this->buffer_.begin());
}
}
}
} // namespace esphome::uart

View File

@@ -1,31 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/event/event.h"
#include "esphome/components/uart/uart.h"
#include <vector>
namespace esphome::uart {
class UARTEvent : public event::Event, public UARTDevice, public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len);
protected:
struct EventMatcher {
const char *event_name;
const uint8_t *data;
size_t data_len;
};
void read_data_();
std::vector<EventMatcher> matchers_;
std::vector<uint8_t> buffer_;
size_t max_matcher_len_ = 0;
};
} // namespace esphome::uart

View File

@@ -53,16 +53,8 @@ static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-N
#endif
// Parse URL and return match info
// URL formats:
// /{domain}/{entity_name} - main device, no method
// /{domain}/{entity_name}/{method} - main device with method
// /{domain}/{device_name}/{entity_name}/{method} - sub-device with method (USE_DEVICES only)
static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) {
UrlMatch match{};
#ifdef USE_DEVICES
match.device_name = nullptr;
match.device_name_len = 0;
#endif
// URL must start with '/'
if (url_len < 2 || url_ptr[0] != '/') {
@@ -89,139 +81,34 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain)
return match;
}
// Parse remaining segments
// Parse ID if present
if (domain_end + 1 >= end) {
return match; // Nothing after domain slash
}
// Find all remaining slashes to count segments
const char *seg1_start = domain_end + 1;
const char *seg1_end = (const char *) memchr(seg1_start, '/', end - seg1_start);
const char *id_start = domain_end + 1;
const char *id_end = (const char *) memchr(id_start, '/', end - id_start);
if (!seg1_end) {
// Only 1 segment after domain: /{domain}/{entity_name}
match.id = seg1_start;
match.id_len = end - seg1_start;
// Reject empty segment (e.g., "/sensor/")
if (match.id_len == 0) {
return UrlMatch{};
}
if (!id_end) {
// No more slashes, entire remaining string is ID
match.id = id_start;
match.id_len = end - id_start;
return match;
}
const char *seg2_start = seg1_end + 1;
const char *seg2_end = (seg2_start < end) ? (const char *) memchr(seg2_start, '/', end - seg2_start) : nullptr;
// Set ID
match.id = id_start;
match.id_len = id_end - id_start;
if (!seg2_end) {
// 2 segments after domain: /{domain}/{X}/{Y}
// This is /{domain}/{entity_name}/{method} for main device
match.id = seg1_start;
match.id_len = seg1_end - seg1_start;
match.method = seg2_start;
match.method_len = end - seg2_start;
// Reject empty segments (e.g., "/sensor//turn_on" or "/sensor/temp/")
if (match.id_len == 0 || match.method_len == 0) {
return UrlMatch{};
}
return match;
// Parse method if present
if (id_end + 1 < end) {
match.method = id_end + 1;
match.method_len = end - (id_end + 1);
}
#ifdef USE_DEVICES
// 3+ segments after domain: /{domain}/{device_name}/{entity_name}/{method}
const char *seg3_start = seg2_end + 1;
match.device_name = seg1_start;
match.device_name_len = seg1_end - seg1_start;
match.id = seg2_start;
match.id_len = seg2_end - seg2_start;
if (seg3_start < end) {
match.method = seg3_start;
match.method_len = end - seg3_start;
} else {
// No method segment - fields already zero-initialized by UrlMatch{}
match.method = nullptr;
match.method_len = 0;
}
// Reject empty segments (e.g., "/sensor//entity/turn_on" or "/sensor/device//turn_on")
if (match.device_name_len == 0 || match.id_len == 0 || (match.method != nullptr && match.method_len == 0)) {
return UrlMatch{};
}
#else
// Without USE_DEVICES, reject URLs with 3+ segments (device paths not supported)
return UrlMatch{};
#endif
return match;
}
EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const {
EntityMatchResult result{false, this->method_len == 0};
#ifdef USE_DEVICES
Device *entity_device = entity->get_device();
bool url_has_device = (this->device_name_len > 0);
bool entity_has_device = (entity_device != nullptr);
if (url_has_device) {
// URL has explicit device segment (3+ segments) - must match device
if (!entity_has_device)
return result;
const char *entity_device_name = entity_device->get_name();
if (this->device_name_len != strlen(entity_device_name) ||
memcmp(this->device_name, entity_device_name, this->device_name_len) != 0)
return result;
} else if (entity_has_device) {
// Entity has device but URL has only 2 segments (id/method)
// Try interpreting as device/entity: id=device_name, method=entity_name
if (this->method_len == 0)
return result; // Need 2 segments for this interpretation
const char *entity_device_name = entity_device->get_name();
if (this->id_len == strlen(entity_device_name) && memcmp(this->id, entity_device_name, this->id_len) == 0) {
const StringRef &name_ref = entity->get_name();
if (this->method_len == name_ref.size() && memcmp(this->method, name_ref.c_str(), this->method_len) == 0) {
// Matched: id=device, method=entity_name, so method is effectively empty
return {true, true};
}
}
return result; // No match
}
#endif
// Try matching by entity name (new format)
const StringRef &name_ref = entity->get_name();
if (this->id_matches(name_ref.c_str(), name_ref.size())) {
result.matched = true;
return result;
}
// Fall back to object_id (deprecated format)
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = entity->get_object_id_to(object_id_buf);
if (this->id_matches(object_id.c_str(), object_id.size())) {
result.matched = true;
// Log deprecation warning
#ifdef USE_DEVICES
Device *device = entity->get_device();
if (device != nullptr) {
ESP_LOGW(TAG,
"Deprecated URL format: /%.*s/%.*s/%.*s - use entity name '/%.*s/%s/%s' instead. "
"Object ID URLs will be removed in 2026.7.0.",
this->domain_len, this->domain, this->device_name_len, this->device_name, this->id_len, this->id,
this->domain_len, this->domain, device->get_name(), entity->get_name().c_str());
} else
#endif
{
ESP_LOGW(TAG,
"Deprecated URL format: /%.*s/%.*s - use entity name '/%.*s/%s' instead. "
"Object ID URLs will be removed in 2026.7.0.",
this->domain_len, this->domain, this->id_len, this->id, this->domain_len, this->domain,
entity->get_name().c_str());
}
}
return result;
}
#if !defined(USE_ESP32) && defined(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) {
@@ -400,9 +287,7 @@ std::string WebServer::get_config_json() {
JsonObject root = builder.root();
root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
char comment_buffer[ESPHOME_COMMENT_SIZE];
App.get_comment_string(comment_buffer);
root[ESPHOME_F("comment")] = comment_buffer;
root[ESPHOME_F("comment")] = App.get_comment_ref();
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
#else
@@ -518,53 +403,13 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
#endif
// Helper functions to reduce code size by avoiding macro expansion
// Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name}
// Uses names (not object_id) to avoid UTF-8 collision issues
static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
const StringRef &name = obj->get_name();
size_t prefix_len = strlen(prefix);
size_t name_len = name.size();
#ifdef USE_DEVICES
Device *device = obj->get_device();
const char *device_name = device ? device->get_name() : nullptr;
size_t device_len = device_name ? strlen(device_name) : 0;
#endif
// Build id into stack buffer - ArduinoJson copies the string
// Format: {prefix}/{device?}/{name}
// Buffer size guaranteed by schema validation (NAME_MAX_LENGTH=120):
// With devices: domain(20) + "/" + device(120) + "/" + name(120) + null = 263, rounded up to 280 for safety margin
// Without devices: domain(20) + "/" + name(120) + null = 142, rounded up to 150 for safety margin
#ifdef USE_DEVICES
char id_buf[280];
#else
char id_buf[150];
#endif
char *p = id_buf;
memcpy(p, prefix, prefix_len);
p += prefix_len;
*p++ = '/';
#ifdef USE_DEVICES
if (device_name) {
memcpy(p, device_name, device_len);
p += device_len;
*p++ = '/';
}
#endif
memcpy(p, name.c_str(), name_len);
p[name_len] = '\0';
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[ESPHOME_F("id")] = id_buf;
if (start_config == DETAIL_ALL) {
root[ESPHOME_F("domain")] = prefix;
root[ESPHOME_F("name")] = name;
#ifdef USE_DEVICES
if (device_name) {
root[ESPHOME_F("device")] = device_name;
}
#endif
root[ESPHOME_F("name")] = obj->get_name();
root[ESPHOME_F("icon")] = obj->get_icon_ref();
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
@@ -583,7 +428,7 @@ static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix
}
template<typename T>
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const char *state,
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);
root[ESPHOME_F("state")] = state;
@@ -603,13 +448,12 @@ void WebServer::on_sensor_update(sensor::Sensor *obj) {
}
void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (sensor::Sensor *obj : App.get_sensors()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
if (match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->sensor_json_(obj, obj->state, detail);
std::string data = this->sensor_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -617,20 +461,19 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM
request->send(404);
}
std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE);
return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE);
}
std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL);
return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL);
}
std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) {
std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
const auto uom_ref = obj->get_unit_of_measurement_ref();
char buf[VALUE_ACCURACY_MAX_LEN];
const char *state = std::isnan(value)
? "NA"
: (value_accuracy_with_uom_to_buf(buf, value, obj->get_accuracy_decimals(), uom_ref), buf);
std::string state =
std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref);
set_json_icon_state_value(root, obj, "sensor", state, value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
@@ -650,13 +493,12 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) {
}
void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (text_sensor::TextSensor *obj : App.get_text_sensors()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
if (match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->text_sensor_json_(obj, obj->state, detail);
std::string data = this->text_sensor_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -664,19 +506,19 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const
request->send(404);
}
std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_STATE);
return web_server->text_sensor_json((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_STATE);
}
std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_ALL);
return web_server->text_sensor_json((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_ALL);
}
std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config) {
std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "text_sensor", value.c_str(), value.c_str(), start_config);
set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
@@ -693,13 +535,12 @@ void WebServer::on_switch_update(switch_::Switch *obj) {
}
void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (switch_::Switch *obj : App.get_switches()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->switch_json_(obj, obj->state, detail);
std::string data = this->switch_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -741,12 +582,12 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
request->send(404);
}
std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) {
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE);
return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE);
}
std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) {
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL);
return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL);
}
std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) {
std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -763,12 +604,11 @@ std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail
#ifdef USE_BUTTON
void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (button::Button *obj : App.get_buttons()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->button_json_(obj, detail);
std::string data = this->button_json(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals("press")) {
this->defer([obj]() { obj->press(); });
@@ -782,12 +622,12 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
request->send(404);
}
std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_STATE);
return web_server->button_json((button::Button *) (source), DETAIL_STATE);
}
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
return web_server->button_json((button::Button *) (source), DETAIL_ALL);
}
std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) {
std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -808,13 +648,12 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
}
void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
if (match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->binary_sensor_json_(obj, obj->state, detail);
std::string data = this->binary_sensor_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -822,14 +661,14 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
request->send(404);
}
std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE);
return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE);
}
std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL);
return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL);
}
std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -850,13 +689,12 @@ void WebServer::on_fan_update(fan::Fan *obj) {
}
void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (fan::Fan *obj : App.get_fans()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->fan_json_(obj, detail);
std::string data = this->fan_json(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals("toggle")) {
this->defer([obj]() { obj->toggle().perform(); });
@@ -898,12 +736,12 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
request->send(404);
}
std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE);
return web_server->fan_json((fan::Fan *) (source), DETAIL_STATE);
}
std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL);
return web_server->fan_json((fan::Fan *) (source), DETAIL_ALL);
}
std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) {
std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -931,13 +769,12 @@ void WebServer::on_light_update(light::LightState *obj) {
}
void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (light::LightState *obj : App.get_lights()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->light_json_(obj, detail);
std::string data = this->light_json(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals("toggle")) {
this->defer([obj]() { obj->toggle().perform(); });
@@ -977,12 +814,12 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
request->send(404);
}
std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) {
return web_server->light_json_((light::LightState *) (source), DETAIL_STATE);
return web_server->light_json((light::LightState *) (source), DETAIL_STATE);
}
std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) {
return web_server->light_json_((light::LightState *) (source), DETAIL_ALL);
return web_server->light_json((light::LightState *) (source), DETAIL_ALL);
}
std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) {
std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1010,13 +847,12 @@ void WebServer::on_cover_update(cover::Cover *obj) {
}
void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (cover::Cover *obj : App.get_covers()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->cover_json_(obj, detail);
std::string data = this->cover_json(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1065,12 +901,12 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
request->send(404);
}
std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE);
return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE);
}
std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL);
return web_server->cover_json((cover::Cover *) (source), DETAIL_ALL);
}
std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) {
std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1099,13 +935,12 @@ void WebServer::on_number_update(number::Number *obj) {
}
void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_numbers()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->number_json_(obj, obj->state, detail);
std::string data = this->number_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1125,30 +960,31 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
}
std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) {
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE);
return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE);
}
std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) {
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL);
return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL);
}
std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) {
std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
const auto uom_ref = obj->traits.get_unit_of_measurement_ref();
const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step());
// Need two buffers: one for value, one for state with UOM
char val_buf[VALUE_ACCURACY_MAX_LEN];
char state_buf[VALUE_ACCURACY_MAX_LEN];
const char *val_str = std::isnan(value) ? "\"NaN\"" : (value_accuracy_to_buf(val_buf, value, accuracy), val_buf);
const char *state_str =
std::isnan(value) ? "NA" : (value_accuracy_with_uom_to_buf(state_buf, value, accuracy, uom_ref), state_buf);
std::string val_str = std::isnan(value)
? "\"NaN\""
: value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()));
std::string state_str = std::isnan(value) ? "NA"
: value_accuracy_with_uom_to_string(
value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref);
set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config);
if (start_config == DETAIL_ALL) {
// ArduinoJson copies the string immediately, so we can reuse val_buf
root[ESPHOME_F("min_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_min_value(), accuracy), val_buf);
root[ESPHOME_F("max_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_max_value(), accuracy), val_buf);
root[ESPHOME_F("step")] = (value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy), val_buf);
root[ESPHOME_F("min_value")] =
value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step()));
root[ESPHOME_F("max_value")] =
value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step()));
root[ESPHOME_F("step")] =
value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step()));
root[ESPHOME_F("mode")] = (int) obj->traits.get_mode();
if (!uom_ref.empty())
root[ESPHOME_F("uom")] = uom_ref;
@@ -1167,12 +1003,11 @@ void WebServer::on_date_update(datetime::DateEntity *obj) {
}
void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_dates()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->date_json_(obj, detail);
std::string data = this->date_json(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1198,12 +1033,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
}
std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) {
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE);
return web_server->date_json((datetime::DateEntity *) (source), DETAIL_STATE);
}
std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) {
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL);
return web_server->date_json((datetime::DateEntity *) (source), DETAIL_ALL);
}
std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) {
std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1231,12 +1066,11 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) {
}
void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_times()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->time_json_(obj, detail);
std::string data = this->time_json(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1261,12 +1095,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
request->send(404);
}
std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) {
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE);
return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_STATE);
}
std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) {
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL);
return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_ALL);
}
std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) {
std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1294,12 +1128,11 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) {
}
void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_datetimes()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->datetime_json_(obj, detail);
std::string data = this->datetime_json(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1324,12 +1157,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
request->send(404);
}
std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE);
return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_STATE);
}
std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL);
return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_ALL);
}
std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) {
std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1359,13 +1192,12 @@ void WebServer::on_text_update(text::Text *obj) {
}
void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_texts()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->text_json_(obj, obj->state, detail);
std::string data = this->text_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1385,17 +1217,17 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
}
std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) {
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE);
return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE);
}
std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) {
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL);
return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL);
}
std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) {
std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str();
set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config);
std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value;
set_json_icon_state_value(root, obj, "text", state, value, start_config);
root[ESPHOME_F("min_length")] = obj->traits.get_min_length();
root[ESPHOME_F("max_length")] = obj->traits.get_max_length();
root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str();
@@ -1416,13 +1248,12 @@ void WebServer::on_select_update(select::Select *obj) {
}
void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_selects()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : "", detail);
std::string data = this->select_json(obj, obj->has_state() ? obj->current_option() : "", detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1443,13 +1274,13 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
}
std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) {
auto *obj = (select::Select *) (source);
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE);
return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE);
}
std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) {
auto *obj = (select::Select *) (source);
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL);
return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL);
}
std::string WebServer::select_json_(select::Select *obj, const char *value, JsonDetail start_config) {
std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1474,13 +1305,12 @@ void WebServer::on_climate_update(climate::Climate *obj) {
}
void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_climates()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->climate_json_(obj, detail);
std::string data = this->climate_json(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1510,13 +1340,13 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
}
std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE);
return web_server->climate_json((climate::Climate *) (source), DETAIL_STATE);
}
std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL);
return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
}
std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) {
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1525,7 +1355,6 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
char buf[PSTR_LOCAL_SIZE];
char temp_buf[VALUE_ACCURACY_MAX_LEN];
if (start_config == DETAIL_ALL) {
JsonArray opt = root[ESPHOME_F("modes")].to<JsonArray>();
@@ -1562,10 +1391,8 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con
bool has_state = false;
root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode));
root[ESPHOME_F("max_temp")] =
(value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf);
root[ESPHOME_F("min_temp")] =
(value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf);
root[ESPHOME_F("max_temp")] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy);
root[ESPHOME_F("min_temp")] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step();
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action));
@@ -1588,26 +1415,23 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con
root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode));
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
root[ESPHOME_F("current_temperature")] =
std::isnan(obj->current_temperature)
? "NA"
: (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf);
if (!std::isnan(obj->current_temperature)) {
root[ESPHOME_F("current_temperature")] = value_accuracy_to_string(obj->current_temperature, current_accuracy);
} else {
root[ESPHOME_F("current_temperature")] = "NA";
}
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
root[ESPHOME_F("target_temperature_low")] =
(value_accuracy_to_buf(temp_buf, obj->target_temperature_low, target_accuracy), temp_buf);
root[ESPHOME_F("target_temperature_low")] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
root[ESPHOME_F("target_temperature_high")] =
(value_accuracy_to_buf(temp_buf, obj->target_temperature_high, target_accuracy), temp_buf);
value_accuracy_to_string(obj->target_temperature_high, target_accuracy);
if (!has_state) {
root[ESPHOME_F("state")] =
(value_accuracy_to_buf(temp_buf, (obj->target_temperature_high + obj->target_temperature_low) / 2.0f,
target_accuracy),
temp_buf);
root[ESPHOME_F("state")] = value_accuracy_to_string(
(obj->target_temperature_high + obj->target_temperature_low) / 2.0f, target_accuracy);
}
} else {
root[ESPHOME_F("target_temperature")] =
(value_accuracy_to_buf(temp_buf, obj->target_temperature, target_accuracy), temp_buf);
root[ESPHOME_F("target_temperature")] = value_accuracy_to_string(obj->target_temperature, target_accuracy);
if (!has_state)
root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")];
}
@@ -1625,13 +1449,12 @@ void WebServer::on_lock_update(lock::Lock *obj) {
}
void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (lock::Lock *obj : App.get_locks()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->lock_json_(obj, obj->state, detail);
std::string data = this->lock_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1673,12 +1496,12 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
request->send(404);
}
std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) {
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE);
return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE);
}
std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) {
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL);
return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL);
}
std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1700,13 +1523,12 @@ void WebServer::on_valve_update(valve::Valve *obj) {
}
void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (valve::Valve *obj : App.get_valves()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->valve_json_(obj, detail);
std::string data = this->valve_json(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1753,12 +1575,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
request->send(404);
}
std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE);
return web_server->valve_json((valve::Valve *) (source), DETAIL_STATE);
}
std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL);
return web_server->valve_json((valve::Valve *) (source), DETAIL_ALL);
}
std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) {
std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1785,13 +1607,12 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP
}
void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1832,18 +1653,18 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
request->send(404);
}
std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) {
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_STATE);
return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_STATE);
}
std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) {
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_ALL);
return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_ALL);
}
std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config) {
std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1867,14 +1688,13 @@ void WebServer::on_event(event::Event *obj) {
void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (event::Event *obj : App.get_events()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
if (match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->event_json_(obj, "", detail);
std::string data = this->event_json(obj, "", detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1889,14 +1709,14 @@ static std::string get_event_type(event::Event *event) {
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
auto *event = static_cast<event::Event *>(source);
return web_server->event_json_(event, get_event_type(event), DETAIL_STATE);
return web_server->event_json(event, get_event_type(event), DETAIL_STATE);
}
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) {
auto *event = static_cast<event::Event *>(source);
return web_server->event_json_(event, get_event_type(event), DETAIL_ALL);
return web_server->event_json(event, get_event_type(event), DETAIL_ALL);
}
std::string WebServer::event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config) {
std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1937,13 +1757,12 @@ void WebServer::on_update(update::UpdateEntity *obj) {
}
void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (update::UpdateEntity *obj : App.get_updates()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
if (!match.id_equals_entity(obj))
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->update_json_(obj, detail);
std::string data = this->update_json(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1961,13 +1780,13 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
}
std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
}
std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
}
std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder;
JsonObject root = builder.root();

View File

@@ -36,44 +36,37 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE;
namespace esphome {
namespace web_server {
/// Result of matching a URL against an entity
struct EntityMatchResult {
bool matched; ///< True if entity matched the URL
bool action_is_empty; ///< True if no action in URL (or action field was used as entity name for 2-seg subdevice)
};
/// Internal helper struct that is used to parse incoming URLs
/// Note: Length fields use uint8_t, so NAME_MAX_LENGTH in config_validation.py must stay < 255
struct UrlMatch {
const char *domain; ///< Pointer to domain within URL, for example "sensor"
const char *id; ///< Pointer to entity name/id within URL, for example "Temperature"
const char *id; ///< Pointer to id within URL, for example "living_room_fan"
const char *method; ///< Pointer to method within URL, for example "turn_on"
#ifdef USE_DEVICES
const char *device_name; ///< Pointer to device name within URL, or nullptr for main device
#endif
uint8_t domain_len; ///< Length of domain string
uint8_t id_len; ///< Length of id string (NAME_MAX_LENGTH must be < 255)
uint8_t id_len; ///< Length of id string
uint8_t method_len; ///< Length of method string
#ifdef USE_DEVICES
uint8_t device_name_len; ///< Length of device name string (NAME_MAX_LENGTH must be < 255)
#endif
bool valid; ///< Whether this match is valid
bool valid; ///< Whether this match is valid
// Helper methods for string comparisons
bool domain_equals(const char *str) const {
return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0;
}
/// Check if URL id segment matches a string (by pointer and length)
bool id_matches(const char *str, size_t len) const { return id && id_len == len && memcmp(id, str, len) == 0; }
/// Match entity by name first, then fall back to object_id with deprecation warning
/// Returns EntityMatchResult with match status and whether method is effectively empty
EntityMatchResult match_entity(EntityBase *entity) const;
bool id_equals_entity(EntityBase *entity) const {
// Zero-copy comparison using StringRef
StringRef static_ref = entity->get_object_id_ref_for_api_();
if (!static_ref.empty()) {
return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0;
}
// Fallback to allocation (rare)
const auto &obj_id = entity->get_object_id();
return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0;
}
bool method_equals(const char *str) const {
return method && method_len == strlen(str) && memcmp(method, str, method_len) == 0;
}
bool method_empty() const { return method_len == 0; }
};
#ifdef USE_WEBSERVER_SORTING
@@ -282,6 +275,8 @@ class WebServer : public Controller,
static std::string sensor_state_json_generator(WebServer *web_server, void *source);
static std::string sensor_all_json_generator(WebServer *web_server, void *source);
/// Dump the sensor state with its value as a JSON string.
std::string sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config);
#endif
#ifdef USE_SWITCH
@@ -292,6 +287,8 @@ class WebServer : public Controller,
static std::string switch_state_json_generator(WebServer *web_server, void *source);
static std::string switch_all_json_generator(WebServer *web_server, void *source);
/// Dump the switch state with its value as a JSON string.
std::string switch_json(switch_::Switch *obj, bool value, JsonDetail start_config);
#endif
#ifdef USE_BUTTON
@@ -300,6 +297,8 @@ class WebServer : public Controller,
static std::string button_state_json_generator(WebServer *web_server, void *source);
static std::string button_all_json_generator(WebServer *web_server, void *source);
/// Dump the button details with its value as a JSON string.
std::string button_json(button::Button *obj, JsonDetail start_config);
#endif
#ifdef USE_BINARY_SENSOR
@@ -310,6 +309,8 @@ class WebServer : public Controller,
static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source);
static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source);
/// Dump the binary sensor state with its value as a JSON string.
std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config);
#endif
#ifdef USE_FAN
@@ -320,6 +321,8 @@ class WebServer : public Controller,
static std::string fan_state_json_generator(WebServer *web_server, void *source);
static std::string fan_all_json_generator(WebServer *web_server, void *source);
/// Dump the fan state as a JSON string.
std::string fan_json(fan::Fan *obj, JsonDetail start_config);
#endif
#ifdef USE_LIGHT
@@ -330,6 +333,8 @@ class WebServer : public Controller,
static std::string light_state_json_generator(WebServer *web_server, void *source);
static std::string light_all_json_generator(WebServer *web_server, void *source);
/// Dump the light state as a JSON string.
std::string light_json(light::LightState *obj, JsonDetail start_config);
#endif
#ifdef USE_TEXT_SENSOR
@@ -340,6 +345,8 @@ class WebServer : public Controller,
static std::string text_sensor_state_json_generator(WebServer *web_server, void *source);
static std::string text_sensor_all_json_generator(WebServer *web_server, void *source);
/// Dump the text sensor state with its value as a JSON string.
std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config);
#endif
#ifdef USE_COVER
@@ -350,6 +357,8 @@ class WebServer : public Controller,
static std::string cover_state_json_generator(WebServer *web_server, void *source);
static std::string cover_all_json_generator(WebServer *web_server, void *source);
/// Dump the cover state as a JSON string.
std::string cover_json(cover::Cover *obj, JsonDetail start_config);
#endif
#ifdef USE_NUMBER
@@ -359,6 +368,8 @@ class WebServer : public Controller,
static std::string number_state_json_generator(WebServer *web_server, void *source);
static std::string number_all_json_generator(WebServer *web_server, void *source);
/// Dump the number state with its value as a JSON string.
std::string number_json(number::Number *obj, float value, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_DATE
@@ -368,6 +379,8 @@ class WebServer : public Controller,
static std::string date_state_json_generator(WebServer *web_server, void *source);
static std::string date_all_json_generator(WebServer *web_server, void *source);
/// Dump the date state with its value as a JSON string.
std::string date_json(datetime::DateEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_TIME
@@ -377,6 +390,8 @@ class WebServer : public Controller,
static std::string time_state_json_generator(WebServer *web_server, void *source);
static std::string time_all_json_generator(WebServer *web_server, void *source);
/// Dump the time state with its value as a JSON string.
std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_DATETIME
@@ -386,6 +401,8 @@ class WebServer : public Controller,
static std::string datetime_state_json_generator(WebServer *web_server, void *source);
static std::string datetime_all_json_generator(WebServer *web_server, void *source);
/// Dump the datetime state with its value as a JSON string.
std::string datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_TEXT
@@ -395,6 +412,8 @@ class WebServer : public Controller,
static std::string text_state_json_generator(WebServer *web_server, void *source);
static std::string text_all_json_generator(WebServer *web_server, void *source);
/// Dump the text state with its value as a JSON string.
std::string text_json(text::Text *obj, const std::string &value, JsonDetail start_config);
#endif
#ifdef USE_SELECT
@@ -404,6 +423,8 @@ class WebServer : public Controller,
static std::string select_state_json_generator(WebServer *web_server, void *source);
static std::string select_all_json_generator(WebServer *web_server, void *source);
/// Dump the select state with its value as a JSON string.
std::string select_json(select::Select *obj, const char *value, JsonDetail start_config);
#endif
#ifdef USE_CLIMATE
@@ -413,6 +434,8 @@ class WebServer : public Controller,
static std::string climate_state_json_generator(WebServer *web_server, void *source);
static std::string climate_all_json_generator(WebServer *web_server, void *source);
/// Dump the climate details
std::string climate_json(climate::Climate *obj, JsonDetail start_config);
#endif
#ifdef USE_LOCK
@@ -423,6 +446,8 @@ class WebServer : public Controller,
static std::string lock_state_json_generator(WebServer *web_server, void *source);
static std::string lock_all_json_generator(WebServer *web_server, void *source);
/// Dump the lock state with its value as a JSON string.
std::string lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
#endif
#ifdef USE_VALVE
@@ -433,6 +458,8 @@ class WebServer : public Controller,
static std::string valve_state_json_generator(WebServer *web_server, void *source);
static std::string valve_all_json_generator(WebServer *web_server, void *source);
/// Dump the valve state as a JSON string.
std::string valve_json(valve::Valve *obj, JsonDetail start_config);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
@@ -443,6 +470,9 @@ class WebServer : public Controller,
static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source);
static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
/// Dump the alarm_control_panel state with its value as a JSON string.
std::string alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config);
#endif
#ifdef USE_EVENT
@@ -453,6 +483,9 @@ class WebServer : public Controller,
/// Handle a event request under '/event<id>'.
void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match);
/// Dump the event details with its value as a JSON string.
std::string event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config);
#endif
#ifdef USE_UPDATE
@@ -463,6 +496,8 @@ class WebServer : public Controller,
static std::string update_state_json_generator(WebServer *web_server, void *source);
static std::string update_all_json_generator(WebServer *web_server, void *source);
/// Dump the update state with its value as a JSON string.
std::string update_json(update::UpdateEntity *obj, JsonDetail start_config);
#endif
/// Override the web handler's canHandle method.
@@ -562,69 +597,6 @@ class WebServer : public Controller,
const char *js_include_{nullptr};
#endif
bool expose_log_{true};
private:
#ifdef USE_SENSOR
std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
#endif
#ifdef USE_SWITCH
std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
#endif
#ifdef USE_BUTTON
std::string button_json_(button::Button *obj, JsonDetail start_config);
#endif
#ifdef USE_BINARY_SENSOR
std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config);
#endif
#ifdef USE_FAN
std::string fan_json_(fan::Fan *obj, JsonDetail start_config);
#endif
#ifdef USE_LIGHT
std::string light_json_(light::LightState *obj, JsonDetail start_config);
#endif
#ifdef USE_TEXT_SENSOR
std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config);
#endif
#ifdef USE_COVER
std::string cover_json_(cover::Cover *obj, JsonDetail start_config);
#endif
#ifdef USE_NUMBER
std::string number_json_(number::Number *obj, float value, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_DATE
std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_TIME
std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_DATETIME
std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_TEXT
std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
#endif
#ifdef USE_SELECT
std::string select_json_(select::Select *obj, const char *value, JsonDetail start_config);
#endif
#ifdef USE_CLIMATE
std::string climate_json_(climate::Climate *obj, JsonDetail start_config);
#endif
#ifdef USE_LOCK
std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
#endif
#ifdef USE_VALVE
std::string valve_json_(valve::Valve *obj, JsonDetail start_config);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config);
#endif
#ifdef USE_EVENT
std::string event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config);
#endif
#ifdef USE_UPDATE
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
#endif
};
} // namespace web_server

View File

@@ -6,29 +6,6 @@
namespace esphome {
namespace web_server {
// Write HTML-escaped text to stream (escapes ", &, <, >)
static void write_html_escaped(AsyncResponseStream *stream, const char *text) {
for (const char *p = text; *p; ++p) {
switch (*p) {
case '"':
stream->print("&quot;");
break;
case '&':
stream->print("&amp;");
break;
case '<':
stream->print("&lt;");
break;
case '>':
stream->print("&gt;");
break;
default:
stream->write(*p);
break;
}
}
}
void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action,
const std::function<void(AsyncResponseStream &stream, EntityBase *obj)> &action_func = nullptr) {
stream->print("<tr class=\"");
@@ -38,29 +15,9 @@ void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &
stream->print("\" id=\"");
stream->print(klass.c_str());
stream->print("-");
char object_id_buf[OBJECT_ID_MAX_LEN];
stream->print(obj->get_object_id_to(object_id_buf).c_str());
// Add data attributes for hierarchical URL support
stream->print("\" data-domain=\"");
stream->print(klass.c_str());
stream->print("\" data-name=\"");
write_html_escaped(stream, obj->get_name().c_str());
#ifdef USE_DEVICES
Device *device = obj->get_device();
if (device != nullptr) {
stream->print("\" data-device=\"");
write_html_escaped(stream, device->get_name());
}
#endif
stream->print(obj->get_object_id().c_str());
stream->print("\"><td>");
#ifdef USE_DEVICES
if (device != nullptr) {
stream->print("[");
write_html_escaped(stream, device->get_name());
stream->print("] ");
}
#endif
write_html_escaped(stream, obj->get_name().c_str());
stream->print(obj->get_name().c_str());
stream->print("</td><td></td><td>");
stream->print(action.c_str());
if (action_func) {

View File

@@ -13,8 +13,7 @@ namespace web_server_idf {
static const char *const TAG = "web_server_idf_utils";
size_t url_decode(char *str) {
char *start = str;
void url_decode(char *str) {
char *ptr = str, buf;
for (; *str; str++, ptr++) {
if (*str == '%') {
@@ -32,8 +31,7 @@ size_t url_decode(char *str) {
*ptr = *str;
}
}
*ptr = '\0';
return ptr - start;
*ptr = *str;
}
bool request_has_header(httpd_req_t *req, const char *name) { return httpd_req_get_hdr_value_len(req, name); }

View File

@@ -8,10 +8,6 @@
namespace esphome {
namespace web_server_idf {
/// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space)
/// Returns the new length of the decoded string
size_t url_decode(char *str);
bool request_has_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_url_query(httpd_req_t *req);

View File

@@ -247,20 +247,11 @@ optional<std::string> AsyncWebServerRequest::get_header(const char *name) const
}
std::string AsyncWebServerRequest::url() const {
auto *query_start = strchr(this->req_->uri, '?');
std::string result;
if (query_start == nullptr) {
result = this->req_->uri;
} else {
result = std::string(this->req_->uri, query_start - this->req_->uri);
auto *str = strchr(this->req_->uri, '?');
if (str == nullptr) {
return this->req_->uri;
}
// Decode URL-encoded characters in-place (e.g., %20 -> space)
// This matches AsyncWebServer behavior on Arduino
if (!result.empty()) {
size_t new_len = url_decode(&result[0]);
result.resize(new_len);
}
return result;
return std::string(this->req_->uri, str - this->req_->uri);
}
std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); }
@@ -394,9 +385,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
void AsyncResponseStream::print(float value) {
// Use stack buffer to avoid temporary string allocation
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
constexpr size_t float_buf_size = 32;
char buf[float_buf_size];
int len = snprintf(buf, float_buf_size, "%f", value);
char buf[32];
int len = snprintf(buf, sizeof(buf), "%f", value);
this->content_.append(buf, len);
}

View File

@@ -348,6 +348,10 @@ CONFIG_SCHEMA = cv.All(
cv.boolean, cv.only_on_esp32
),
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
cv.Optional("enable_mdns"): cv.invalid(
"This option has been removed. Please use the [disabled] option under the "
"new mdns component instead."
),
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
@@ -464,7 +468,7 @@ async def to_code(config):
)
cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT]))
cg.add_define("USE_WIFI_AP")
elif CORE.is_esp32 and not CORE.using_arduino:
elif CORE.is_esp32 and CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False)
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
@@ -509,7 +513,7 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
# Apply high performance WiFi settings if high performance networking is enabled
if CORE.is_esp32 and has_high_performance_networking():
if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking():
# Check if PSRAM is guaranteed (set by psram component during final validation)
psram_guaranteed = psram_is_guaranteed()

View File

@@ -1546,7 +1546,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
this->set_sta_priority(failed_bssid.value(), new_priority);
}
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s,

View File

@@ -1972,26 +1972,6 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend(
)
def _validate_no_slash(value):
"""Validate that a name does not contain '/' characters.
The '/' character is used as a path separator in web server URLs,
so it cannot be used in entity or device names.
"""
if "/" in value:
raise Invalid(
f"Name cannot contain '/' character (used as URL path separator): {value}"
)
return value
# Maximum length for entity, device, and area names
# This ensures web server URL IDs fit in a 280-byte buffer:
# domain(20) + "/" + device(120) + "/" + name(120) + null = 263 bytes
# Note: Must be < 255 because web_server UrlMatch uses uint8_t for length fields
NAME_MAX_LENGTH = 120
def _validate_entity_name(value):
value = string(value)
try:
@@ -2002,28 +1982,9 @@ def _validate_entity_name(value):
requires_friendly_name(
"Name cannot be None when esphome->friendly_name is not set!"
)(value)
if value is not None:
# Validate length for web server URL compatibility
if len(value) > NAME_MAX_LENGTH:
raise Invalid(
f"Name is too long ({len(value)} chars). "
f"Maximum length is {NAME_MAX_LENGTH} characters."
)
# Validate no '/' in name for web server URL compatibility
_validate_no_slash(value)
return value
def string_no_slash(value):
"""Validate a string that cannot contain '/' characters.
Used for device and area names where '/' is reserved as a URL path separator.
Use with cv.Length() to also enforce maximum length.
"""
value = string(value)
return _validate_no_slash(value)
ENTITY_BASE_SCHEMA = Schema(
{
Optional(CONF_NAME): _validate_entity_name,

View File

@@ -742,15 +742,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:
@@ -807,11 +798,6 @@ class EsphomeCore:
@property
def using_esp_idf(self):
_LOGGER.warning(
"CORE.using_esp_idf was deprecated in 2026.1, will change behavior in 2026.6. "
"ESP32 Arduino builds on top of ESP-IDF, so ESP-IDF features are available in both frameworks. "
"Use CORE.is_esp32 and/or CORE.using_arduino instead."
)
return self.target_framework == "esp-idf"
@property

View File

@@ -12,7 +12,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/progmem.h"
#include "esphome/core/scheduler.h"
#include "esphome/core/string_ref.h"
#include "esphome/core/version.h"
@@ -108,7 +107,8 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick
class Application {
public:
void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) {
void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment,
bool name_add_mac_suffix) {
arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix;
if (name_add_mac_suffix) {
@@ -127,6 +127,7 @@ class Application {
this->name_ = name;
this->friendly_name_ = friendly_name;
}
this->comment_ = comment;
}
#ifdef USE_DEVICES
@@ -263,19 +264,10 @@ class Application {
return "";
}
/// Copy the comment string into the provided buffer
/// Buffer must be ESPHOME_COMMENT_SIZE bytes (compile-time enforced)
void get_comment_string(std::span<char, ESPHOME_COMMENT_SIZE> buffer) {
ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, buffer.size());
buffer[buffer.size() - 1] = '\0';
}
/// Get the comment of this Application as a string
std::string get_comment() {
char buffer[ESPHOME_COMMENT_SIZE];
this->get_comment_string(buffer);
return std::string(buffer);
}
/// Get the comment of this Application set by pre_setup().
std::string get_comment() const { return this->comment_; }
/// Get the comment as StringRef (avoids allocation)
StringRef get_comment_ref() const { return StringRef(this->comment_); }
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
@@ -521,6 +513,7 @@ class Application {
// Pointer-sized members first
Component *current_component_{nullptr};
const char *comment_{nullptr};
// std::vector (3 pointers each: begin, end, capacity)
// Partitioned vector design for looping components

View File

@@ -7,6 +7,4 @@
#define ESPHOME_CONFIG_HASH 0x12345678U // NOLINT
#define ESPHOME_BUILD_TIME 1700000000 // NOLINT
#define ESPHOME_COMMENT_SIZE 1 // NOLINT
static const char ESPHOME_BUILD_TIME_STR[] = "2024-01-01 00:00:00 +0000";
static const char ESPHOME_COMMENT_STR[] = "";

View File

@@ -186,14 +186,14 @@ else:
AREA_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Area),
cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)),
cv.Required(CONF_NAME): cv.string,
}
)
DEVICE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Device),
cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)),
cv.Required(CONF_NAME): cv.string,
cv.Optional(CONF_AREA_ID): cv.use_id(Area),
}
)
@@ -207,11 +207,9 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.valid_name,
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(
cv.string_no_slash, cv.Length(max=120)
),
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)),
cv.Optional(CONF_AREA): validate_area_config,
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),
cv.Optional(CONF_COMMENT): cv.string,
cv.Required(CONF_BUILD_PATH): cv.string,
cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
{
@@ -507,6 +505,7 @@ async def to_code(config: ConfigType) -> None:
cg.App.pre_setup(
config[CONF_NAME],
config[CONF_FRIENDLY_NAME],
config.get(CONF_COMMENT, ""),
config[CONF_NAME_ADD_MAC_SUFFIX],
)
)

View File

@@ -9,8 +9,7 @@ static const char *const TAG = "entity_base";
// Entity Name
const StringRef &EntityBase::get_name() const { return this->name_; }
void EntityBase::set_name(const char *name) { this->set_name(name, 0); }
void EntityBase::set_name(const char *name, uint32_t object_id_hash) {
void EntityBase::set_name(const char *name) {
this->name_ = StringRef(name);
if (this->name_.empty()) {
#ifdef USE_DEVICES
@@ -19,21 +18,11 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) {
} else
#endif
{
// Use friendly_name if available, otherwise fall back to device name
const std::string &friendly = App.get_friendly_name();
this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name());
this->name_ = StringRef(App.get_friendly_name());
}
this->flags_.has_own_name = false;
// Dynamic name - must calculate hash at runtime
this->calc_object_id_();
} else {
this->flags_.has_own_name = true;
// Static name - use pre-computed hash if provided
if (object_id_hash != 0) {
this->object_id_hash_ = object_id_hash;
} else {
this->calc_object_id_();
}
}
}
@@ -56,30 +45,45 @@ void EntityBase::set_icon(const char *icon) {
#endif
}
// Entity Object ID - computed on-demand from name
// Check if the object_id is dynamic (changes with MAC suffix)
bool EntityBase::is_object_id_dynamic_() const {
return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled();
}
// Entity Object ID
std::string EntityBase::get_object_id() const {
char buf[OBJECT_ID_MAX_LEN];
size_t len = this->write_object_id_to(buf, sizeof(buf));
return std::string(buf, len);
}
// Calculate Object ID Hash directly from name using snake_case + sanitize
void EntityBase::calc_object_id_() {
this->object_id_hash_ = fnv1_hash_object_id(this->name_.c_str(), this->name_.size());
}
size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const {
size_t len = std::min(this->name_.size(), buf_size - 1);
for (size_t i = 0; i < len; i++) {
buf[i] = to_sanitized_char(to_snake_case_char(this->name_[i]));
// Check if `App.get_friendly_name()` is constant or dynamic.
if (this->is_object_id_dynamic_()) {
// `App.get_friendly_name()` is dynamic.
return str_sanitize(str_snake_case(App.get_friendly_name()));
}
buf[len] = '\0';
return len;
// `App.get_friendly_name()` is constant.
return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_;
}
StringRef EntityBase::get_object_id_ref_for_api_() const {
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
// Return empty for dynamic case (MAC suffix)
if (this->is_object_id_dynamic_()) {
return EMPTY_STRING;
}
// For static case, return the string or empty if null
return this->object_id_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->object_id_c_str_);
}
void EntityBase::set_object_id(const char *object_id) {
this->object_id_c_str_ = object_id;
this->calc_object_id_();
}
StringRef EntityBase::get_object_id_to(std::span<char, OBJECT_ID_MAX_LEN> buf) const {
size_t len = this->write_object_id_to(buf.data(), buf.size());
return StringRef(buf.data(), len);
void EntityBase::set_name_and_object_id(const char *name, const char *object_id) {
this->set_name(name);
this->object_id_c_str_ = object_id;
this->calc_object_id_();
}
// Calculate Object ID Hash from Entity Name
void EntityBase::calc_object_id_() {
this->object_id_hash_ =
fnv1_hash(this->is_object_id_dynamic_() ? this->get_object_id().c_str() : this->object_id_c_str_);
}
uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; }

View File

@@ -1,8 +1,7 @@
#pragma once
#include <cstdint>
#include <span>
#include <string>
#include <cstdint>
#include "string_ref.h"
#include "helpers.h"
#include "log.h"
@@ -13,8 +12,14 @@
namespace esphome {
// Maximum size for object_id buffer (friendly_name max ~120 + margin)
static constexpr size_t OBJECT_ID_MAX_LEN = 128;
// Forward declaration for friend access
namespace api {
class APIConnection;
} // namespace api
namespace web_server {
struct UrlMatch;
} // namespace web_server
enum EntityCategory : uint8_t {
ENTITY_CATEGORY_NONE = 0,
@@ -28,37 +33,20 @@ class EntityBase {
// Get/set the name of this Entity
const StringRef &get_name() const;
void set_name(const char *name);
/// Set name with pre-computed object_id hash (avoids runtime hash calculation)
/// Use hash=0 for dynamic names that need runtime calculation
void set_name(const char *name, uint32_t object_id_hash);
// Get whether this Entity has its own name or it should use the device friendly_name.
bool has_own_name() const { return this->flags_.has_own_name; }
// Get the sanitized name of this Entity as an ID.
// Deprecated: object_id mangles names and all object_id methods are planned for removal.
// See https://github.com/esphome/backlog/issues/76
// Now is the time to stop using object_id entirely. If you still need it temporarily,
// use get_object_id_to() which will remain available longer but will also eventually be removed.
ESPDEPRECATED("object_id mangles names and all object_id methods are planned for removal "
"(see https://github.com/esphome/backlog/issues/76). "
"Now is the time to stop using object_id. If still needed, use get_object_id_to() "
"which will remain available longer. get_object_id() will be removed in 2026.7.0",
"2025.12.0")
std::string get_object_id() const;
void set_object_id(const char *object_id);
// Set both name and object_id in one call (reduces generated code size)
void set_name_and_object_id(const char *name, const char *object_id);
// Get the unique Object ID of this Entity
uint32_t get_object_id_hash();
/// Get object_id with zero heap allocation
/// For static case: returns StringRef to internal storage (buffer unused)
/// For dynamic case: formats into buffer and returns StringRef to buffer
StringRef get_object_id_to(std::span<char, OBJECT_ID_MAX_LEN> buf) const;
/// Write object_id directly to buffer, returns length written (excluding null)
/// Useful for building compound strings without intermediate buffer
size_t write_object_id_to(char *buf, size_t buf_size) const;
// Get/set whether this Entity should be hidden outside ESPHome
bool is_internal() const { return this->flags_.internal; }
void set_internal(bool internal) { this->flags_.internal = internal; }
@@ -99,8 +87,6 @@ class EntityBase {
return this->device_->get_device_id();
}
void set_device(Device *device) { this->device_ = device; }
// Get the device this entity belongs to (nullptr if main device)
Device *get_device() const { return this->device_; }
#endif
// Check if this entity has state
@@ -139,9 +125,20 @@ class EntityBase {
}
protected:
friend class api::APIConnection;
friend struct web_server::UrlMatch;
// Get object_id as StringRef when it's static (for API usage)
// Returns empty StringRef if object_id is dynamic (needs allocation)
StringRef get_object_id_ref_for_api_() const;
void calc_object_id_();
/// Check if the object_id is dynamic (changes with MAC suffix)
bool is_object_id_dynamic_() const;
StringRef name_;
const char *object_id_c_str_{nullptr};
#ifdef USE_ENTITY_ICON
const char *icon_c_str_{nullptr};
#endif

View File

@@ -15,7 +15,7 @@ from esphome.const import (
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, add, get_variable
import esphome.final_validate as fv
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
from esphome.helpers import sanitize, snake_case
from esphome.types import ConfigType, EntityMetadata
_LOGGER = logging.getLogger(__name__)
@@ -75,21 +75,34 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
config: Configuration dictionary containing entity settings
platform: The platform name (e.g., "sensor", "binary_sensor")
"""
# Set device if configured
# Get device info
device_name: str | None = None
device_id_obj: ID | None
if device_id_obj := config.get(CONF_DEVICE_ID):
device: MockObj = await get_variable(device_id_obj)
add(var.set_device(device))
# Get device name for object ID calculation
device_name = device_id_obj.id
# Set the entity name with pre-computed object_id hash
# For entities with a name, we pre-compute the hash to avoid runtime calculation
# For empty names (use device friendly_name), pass 0 to compute at runtime
entity_name = config[CONF_NAME]
if entity_name:
object_id_hash = fnv1_hash_object_id(entity_name)
add(var.set_name(entity_name, object_id_hash))
else:
add(var.set_name(entity_name, 0))
# Calculate base object_id using the same logic as C++
# This must match the C++ behavior in esphome/core/entity_base.cpp
base_object_id = get_base_entity_object_id(
config[CONF_NAME], CORE.friendly_name, device_name
)
if not config[CONF_NAME]:
_LOGGER.debug(
"Entity has empty name, using '%s' as object_id base", base_object_id
)
# Set both name and object_id in one call to reduce generated code size
add(var.set_name_and_object_id(config[CONF_NAME], base_object_id))
_LOGGER.debug(
"Setting object_id '%s' for entity '%s' on platform '%s'",
base_object_id,
config[CONF_NAME],
platform,
)
# Only set disabled_by_default if True (default is False)
if config[CONF_DISABLED_BY_DEFAULT]:
add(var.set_disabled_by_default(True))

View File

@@ -189,6 +189,14 @@ template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str)
}
std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); }
std::string str_upper_case(const std::string &str) { return str_ctype_transform<std::toupper>(str); }
// Convert char to snake_case: lowercase and spaces to underscores
static constexpr char to_snake_case_char(char c) {
return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
}
// Sanitize char: keep alphanumerics, dashes, underscores; replace others with underscore
static constexpr char to_sanitized_char(char c) {
return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_';
}
std::string str_snake_case(const std::string &str) {
std::string result = str;
for (char &c : result) {
@@ -236,16 +244,17 @@ std::string str_sprintf(const char *fmt, ...) {
// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term)
static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128;
size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep,
const char *suffix_ptr, size_t suffix_len) {
std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr,
size_t suffix_len) {
char buffer[MAX_NAME_WITH_SUFFIX_SIZE];
size_t total_len = name_len + 1 + suffix_len;
// Silently truncate if needed: prioritize keeping the full suffix
if (total_len >= buffer_size) {
// NOTE: This calculation could underflow if suffix_len >= buffer_size - 2,
if (total_len >= MAX_NAME_WITH_SUFFIX_SIZE) {
// NOTE: This calculation could underflow if suffix_len >= MAX_NAME_WITH_SUFFIX_SIZE - 2,
// but this is safe because this helper is only called with small suffixes:
// MAC suffixes (6-12 bytes), ".local" (5 bytes), etc.
name_len = buffer_size - suffix_len - 2; // -2 for separator and null terminator
name_len = MAX_NAME_WITH_SUFFIX_SIZE - suffix_len - 2; // -2 for separator and null terminator
total_len = name_len + 1 + suffix_len;
}
@@ -253,14 +262,7 @@ size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *na
buffer[name_len] = sep;
memcpy(buffer + name_len + 1, suffix_ptr, suffix_len);
buffer[total_len] = '\0';
return total_len;
}
std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr,
size_t suffix_len) {
char buffer[MAX_NAME_WITH_SUFFIX_SIZE];
size_t len = make_name_with_suffix_to(buffer, sizeof(buffer), name, name_len, sep, suffix_ptr, suffix_len);
return std::string(buffer, len);
return std::string(buffer, total_len);
}
std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) {
@@ -383,33 +385,23 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de
}
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
char buf[VALUE_ACCURACY_MAX_LEN];
value_accuracy_to_buf(buf, value, accuracy_decimals);
return std::string(buf);
}
size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals) {
normalize_accuracy_decimals(value, accuracy_decimals);
// snprintf returns chars that would be written (excluding null), or negative on error
int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value);
if (len < 0)
return 0; // encoding error
// On truncation, snprintf returns would-be length; actual written is buf.size() - 1
return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len);
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);
}
size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value,
int8_t accuracy_decimals, StringRef unit_of_measurement) {
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()) {
return value_accuracy_to_buf(buf, value, accuracy_decimals);
snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value);
} else {
snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str());
}
normalize_accuracy_decimals(value, accuracy_decimals);
// snprintf returns chars that would be written (excluding null), or negative on error
int len = snprintf(buf.data(), buf.size(), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str());
if (len < 0)
return 0; // encoding error
// On truncation, snprintf returns would-be length; actual written is buf.size() - 1
return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len);
return std::string(tmp);
}
int8_t step_to_accuracy_decimals(float step) {
@@ -425,8 +417,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.
@@ -436,8 +430,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

@@ -162,9 +162,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]; }
@@ -519,33 +516,12 @@ std::string str_until(const std::string &str, char ch);
std::string str_lower_case(const std::string &str);
/// Convert the string to upper case.
std::string str_upper_case(const std::string &str);
/// Convert a single char to snake_case: lowercase and space to underscore.
constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; }
/// Convert the string to snake case (lowercase with underscores).
std::string str_snake_case(const std::string &str);
/// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore.
constexpr char to_sanitized_char(char c) {
return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_';
}
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
std::string str_sanitize(const std::string &str);
/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations.
/// This computes object_id hashes directly from names without creating an intermediate buffer.
/// IMPORTANT: Must match Python fnv1_hash_object_id() in esphome/helpers.py.
/// If you modify this function, update the Python version and tests in both places.
inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
uint32_t hash = FNV1_OFFSET_BASIS;
for (size_t i = 0; i < len; i++) {
hash *= FNV1_PRIME;
// Apply snake_case (space->underscore, uppercase->lowercase) then sanitize
hash ^= static_cast<uint8_t>(to_sanitized_char(to_snake_case_char(str[i])));
}
return hash;
}
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
@@ -573,18 +549,6 @@ std::string make_name_with_suffix(const std::string &name, char sep, const char
std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr,
size_t suffix_len);
/// Zero-allocation version: format name + separator + suffix directly into buffer.
/// @param buffer Output buffer (must have space for result + null terminator)
/// @param buffer_size Size of the output buffer
/// @param name The base name string
/// @param name_len Length of the name
/// @param sep Single character separator
/// @param suffix_ptr Pointer to the suffix characters
/// @param suffix_len Length of the suffix
/// @return Length written (excluding null terminator)
size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep,
const char *suffix_ptr, size_t suffix_len);
///@}
/// @name Parsing & formatting
@@ -903,15 +867,8 @@ 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);
/// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null)
static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64;
/// Format value with accuracy to buffer, returns chars written (excluding null)
size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals);
/// Format value with accuracy and UOM to buffer, returns chars written (excluding null)
size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value,
int8_t accuracy_decimals, StringRef unit_of_measurement);
/// 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

@@ -94,9 +94,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

@@ -51,19 +51,15 @@ class AssignmentExpression(Expression):
class VariableDeclarationExpression(Expression):
__slots__ = ("type", "modifier", "name", "static")
__slots__ = ("type", "modifier", "name")
def __init__(
self, type_: "MockObj", modifier: str, name: ID, *, static: bool = False
) -> None:
def __init__(self, type_, modifier, name):
self.type = type_
self.modifier = modifier
self.name = name
self.static = static
def __str__(self) -> str:
prefix = "static " if self.static else ""
return f"{prefix}{self.type} {self.modifier}{self.name}"
def __str__(self):
return f"{self.type} {self.modifier}{self.name}"
class ExpressionList(Expression):
@@ -511,17 +507,13 @@ def with_local_variable(id_: ID, rhs: SafeExpType, callback: Callable, *args) ->
CORE.add(RawStatement("}")) # output closing curly brace
def new_variable(
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, *, static: bool = True
) -> "MockObj":
def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
"""Declare and define a new variable, not pointer type, in the code generation.
:param id_: The ID used to declare the variable.
:param rhs: The expression to place on the right hand side of the assignment.
:param type_: Manually define a type for the variable, only use this when it's not possible
to do so during config validation phase (for example because of template arguments).
:param static: If True (default), declare with static storage class for optimization.
Set to False when the variable must have external linkage (e.g., to match library declarations).
:return: The new variable as a MockObj.
"""
@@ -530,7 +522,7 @@ def new_variable(
obj = MockObj(id_, ".")
if type_ is not None:
id_.type = type_
decl = VariableDeclarationExpression(id_.type, "", id_, static=static)
decl = VariableDeclarationExpression(id_.type, "", id_)
CORE.add_global(decl)
assignment = AssignmentExpression(None, "", id_, rhs)
CORE.add(assignment)
@@ -552,7 +544,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
obj = MockObj(id_, "->")
if type_ is not None:
id_.type = type_
decl = VariableDeclarationExpression(id_.type, "*", id_, static=True)
decl = VariableDeclarationExpression(id_.type, "*", id_)
CORE.add_global(decl)
assignment = AssignmentExpression(None, None, id_, rhs)
CORE.add(assignment)

View File

@@ -35,10 +35,6 @@ IS_MACOS = platform.system() == "Darwin"
IS_WINDOWS = platform.system() == "Windows"
IS_LINUX = platform.system() == "Linux"
# FNV-1 hash constants (must match C++ in esphome/core/helpers.h)
FNV1_OFFSET_BASIS = 2166136261
FNV1_PRIME = 16777619
def ensure_unique_string(preferred_string, current_strings):
test_string = preferred_string
@@ -53,17 +49,8 @@ def ensure_unique_string(preferred_string, current_strings):
return test_string
def fnv1_hash(string: str) -> int:
"""FNV-1 32-bit hash function (multiply then XOR)."""
hash_value = FNV1_OFFSET_BASIS
for char in string:
hash_value = (hash_value * FNV1_PRIME) & 0xFFFFFFFF
hash_value ^= ord(char)
return hash_value
def fnv1a_32bit_hash(string: str) -> int:
"""FNV-1a 32-bit hash function (XOR then multiply).
"""FNV-1a 32-bit hash function.
Note: This uses 32-bit hash instead of 64-bit for several reasons:
1. ESPHome targets 32-bit microcontrollers with limited RAM (often <320KB)
@@ -76,22 +63,13 @@ def fnv1a_32bit_hash(string: str) -> int:
a handful of area_ids and device_ids (typically <10 areas and <100
devices), making collisions virtually impossible.
"""
hash_value = FNV1_OFFSET_BASIS
hash_value = 2166136261
for char in string:
hash_value ^= ord(char)
hash_value = (hash_value * FNV1_PRIME) & 0xFFFFFFFF
hash_value = (hash_value * 16777619) & 0xFFFFFFFF
return hash_value
def fnv1_hash_object_id(name: str) -> int:
"""Compute FNV-1 hash of name with snake_case + sanitize transformations.
IMPORTANT: Must produce same result as C++ fnv1_hash_object_id() in helpers.h.
Used for pre-computing entity object_id hashes at code generation time.
"""
return fnv1_hash(sanitize(snake_case(name)))
def strip_accents(value: str) -> str:
"""Remove accents from a string."""
import unicodedata

View File

@@ -171,16 +171,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):
@@ -429,74 +420,3 @@ class IDEData:
if path.endswith(".exe")
else f"{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.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.helpers import get_esphome_components
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
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 = MemoryAnalyzerCLI(
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

@@ -21,7 +21,6 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError
from esphome.helpers import (
copy_file_if_changed,
cpp_string_escape,
get_str_env,
is_ha_addon,
read_file,
@@ -272,7 +271,7 @@ def copy_src_tree():
"esphome", "core", "build_info_data.h"
)
build_info_json_path = CORE.relative_build_path("build_info.json")
config_hash, build_time, build_time_str, comment = get_build_info()
config_hash, build_time, build_time_str = get_build_info()
# Defensively force a rebuild if the build_info files don't exist, or if
# there was a config change which didn't actually cause a source change
@@ -293,9 +292,7 @@ def copy_src_tree():
if sources_changed:
write_file(
build_info_data_h_path,
generate_build_info_data_h(
config_hash, build_time, build_time_str, comment
),
generate_build_info_data_h(config_hash, build_time, build_time_str),
)
write_file(
build_info_json_path,
@@ -335,39 +332,31 @@ def generate_version_h():
)
def get_build_info() -> tuple[int, int, str, str]:
def get_build_info() -> tuple[int, int, str]:
"""Calculate build_info values from current config.
Returns:
Tuple of (config_hash, build_time, build_time_str, comment)
Tuple of (config_hash, build_time, build_time_str)
"""
config_hash = CORE.config_hash
build_time = int(time.time())
build_time_str = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(build_time))
comment = CORE.comment or ""
return config_hash, build_time, build_time_str, comment
return config_hash, build_time, build_time_str
def generate_build_info_data_h(
config_hash: int, build_time: int, build_time_str: str, comment: str
config_hash: int, build_time: int, build_time_str: str
) -> str:
"""Generate build_info_data.h header with config hash, build time, and comment."""
# cpp_string_escape returns '"escaped"', slice off the quotes since template has them
escaped_comment = cpp_string_escape(comment)[1:-1]
# +1 for null terminator
comment_size = len(comment) + 1
"""Generate build_info_data.h header with config hash and build time."""
return f"""#pragma once
// Auto-generated build_info data
#define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT
#define ESPHOME_BUILD_TIME {build_time} // NOLINT
#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT
#ifdef USE_ESP8266
#include <pgmspace.h>
static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}";
static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}";
#else
static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}";
static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}";
#endif
"""

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