mirror of
https://github.com/esphome/esphome.git
synced 2026-01-31 08:52:07 -07:00
Compare commits
2 Commits
integratio
...
wifi_callb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c27835c03 | ||
|
|
0fd50b2381 |
@@ -1,21 +1 @@
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41
|
||||
=======
|
||||
d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00
|
||||
>>>>>>> mqtt_enum_flash
|
||||
=======
|
||||
a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41
|
||||
>>>>>>> upstream/dev
|
||||
=======
|
||||
08c21fa4c044fd80c8f3296371f30c4f5ff3418f1bc1efe63c5bad938301f122
|
||||
>>>>>>> rp2040_web_server_only_no_cap
|
||||
=======
|
||||
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab
|
||||
>>>>>>> upstream/dev
|
||||
=======
|
||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
||||
>>>>>>> upstream/dev
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -439,28 +438,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
|
||||
|
||||
@@ -45,7 +45,6 @@ service APIConnection {
|
||||
rpc time_command (TimeCommandRequest) returns (void) {}
|
||||
rpc update_command (UpdateCommandRequest) returns (void) {}
|
||||
rpc valve_command (ValveCommandRequest) returns (void) {}
|
||||
rpc water_heater_command (WaterHeaterCommandRequest) returns (void) {}
|
||||
|
||||
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
|
||||
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
|
||||
|
||||
@@ -133,8 +133,8 @@ void APIConnection::start() {
|
||||
return;
|
||||
}
|
||||
// Initialize client name with peername (IP address) until Hello message provides actual name
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
|
||||
const char *peername = this->helper_->get_client_peername();
|
||||
this->helper_->set_client_name(peername, strlen(peername));
|
||||
}
|
||||
|
||||
APIConnection::~APIConnection() {
|
||||
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
|
||||
|
||||
void APIConnection::loop() {
|
||||
if (this->flags_.next_close) {
|
||||
// requested a disconnect - don't close socket here, let APIServer::loop() do it
|
||||
// so getpeername() still works for the disconnect trigger
|
||||
// requested a disconnect
|
||||
this->helper_->close();
|
||||
this->flags_.remove = true;
|
||||
return;
|
||||
}
|
||||
@@ -293,8 +293,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
|
||||
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
|
||||
// Don't close socket here, let APIServer::loop() do it
|
||||
// so getpeername() still works for the disconnect trigger
|
||||
this->helper_->close();
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
|
||||
@@ -1386,7 +1385,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec
|
||||
is_single);
|
||||
}
|
||||
|
||||
void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) {
|
||||
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
|
||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
|
||||
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
|
||||
@@ -1525,11 +1524,8 @@ void APIConnection::complete_authentication_() {
|
||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
{
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
||||
std::string(this->helper_->get_peername_to(peername)));
|
||||
}
|
||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
||||
std::string(this->helper_->get_client_peername()));
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
@@ -1548,9 +1544,8 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
this->client_api_version_minor_ = msg.api_version_minor;
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
|
||||
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
@@ -1867,8 +1862,7 @@ void APIConnection::on_no_setup_connection() {
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
|
||||
}
|
||||
void APIConnection::on_fatal_error() {
|
||||
// Don't close socket here - keep it open so getpeername() works for logging
|
||||
// Socket will be closed when client is removed from the list in APIServer::loop()
|
||||
this->helper_->close();
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
|
||||
@@ -2224,14 +2218,12 @@ void APIConnection::process_state_subscriptions_() {
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
|
||||
void APIConnection::log_client_(int level, const LogString *message) {
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
|
||||
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
|
||||
this->helper_->get_client_peername(), LOG_STR_ARG(message));
|
||||
}
|
||||
|
||||
void APIConnection::log_warning_(const LogString *message, APIError err) {
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
|
||||
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
|
||||
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
|
||||
void water_heater_command(const WaterHeaterCommandRequest &msg) override;
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
@@ -281,10 +281,8 @@ class APIConnection final : public APIServerConnection {
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||
|
||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
||||
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
return this->helper_->get_peername_to(buf);
|
||||
}
|
||||
/// Get peer name (IP address) - cached at connection init time
|
||||
const char *get_peername() const { return this->helper_->get_client_peername(); }
|
||||
|
||||
protected:
|
||||
// Helper function to handle authentication completion
|
||||
|
||||
@@ -16,12 +16,7 @@ static const char *const TAG = "api.frame_helper";
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
@@ -245,20 +240,13 @@ APIError APIFrameHelper::try_send_tx_buf_() {
|
||||
return APIError::OK; // All buffers sent successfully
|
||||
}
|
||||
|
||||
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
if (this->socket_) {
|
||||
this->socket_->getpeername_to(buf);
|
||||
} else {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
APIError APIFrameHelper::init_common_() {
|
||||
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
|
||||
HELPER_LOG("Bad state for init %d", (int) state_);
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
// Cache peername now while socket is valid - needed for error logging after socket failure
|
||||
this->socket_->getpeername_to(this->client_peername_);
|
||||
int err = this->socket_->setblocking(false);
|
||||
if (err != 0) {
|
||||
state_ = State::FAILED;
|
||||
|
||||
@@ -90,9 +90,8 @@ class APIFrameHelper {
|
||||
|
||||
// Get client name (null-terminated)
|
||||
const char *get_client_name() const { return this->client_name_; }
|
||||
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
|
||||
// Returns pointer to buf for convenience in printf-style calls
|
||||
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
|
||||
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
|
||||
const char *get_client_peername() const { return this->client_peername_; }
|
||||
// Set client name from buffer with length (truncates if needed)
|
||||
void set_client_name(const char *name, size_t len) {
|
||||
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
|
||||
@@ -106,8 +105,6 @@ class APIFrameHelper {
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||
APIError close() {
|
||||
if (state_ == State::CLOSED)
|
||||
return APIError::OK; // Already closed
|
||||
state_ = State::CLOSED;
|
||||
int err = this->socket_->close();
|
||||
if (err == -1)
|
||||
@@ -234,6 +231,8 @@ class APIFrameHelper {
|
||||
|
||||
// Client name buffer - stores name from Hello message or initial peername
|
||||
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
||||
// Cached peername/IP address - captured at init time for availability after socket failure
|
||||
char client_peername_[socket::SOCKADDR_STR_LEN]{};
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t rx_buf_len_ = 0;
|
||||
|
||||
@@ -29,12 +29,7 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
@@ -21,12 +21,7 @@ static const char *const TAG = "api.plaintext";
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
@@ -746,11 +746,6 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest &
|
||||
#ifdef USE_VALVE
|
||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
void APIServerConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
|
||||
this->water_heater_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
|
||||
@@ -303,9 +303,6 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_VALVE
|
||||
virtual void valve_command(const ValveCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
virtual void water_heater_command(const WaterHeaterCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
|
||||
#endif
|
||||
@@ -435,9 +432,6 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_VALVE
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
#endif
|
||||
|
||||
@@ -192,15 +192,11 @@ void APIServer::loop() {
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
// Save client info before removal for the trigger
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
std::string client_peername(client->get_peername());
|
||||
#endif
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
|
||||
@@ -25,9 +25,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) {
|
||||
return to_string(std::forward<T>(val)); // NOLINT
|
||||
}
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
|
||||
@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
// Build and send JSON response
|
||||
json::JsonBuilder builder;
|
||||
this->json_builder_(x..., builder.root());
|
||||
auto json_buf = builder.serialize();
|
||||
std::string json_str = builder.serialize();
|
||||
this->parent_->send_action_response(call_id, success, StringRef(error_message),
|
||||
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
|
||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "bedjet_hub.h"
|
||||
#include "bedjet_child.h"
|
||||
#include "bedjet_const.h"
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <cinttypes>
|
||||
|
||||
|
||||
@@ -89,9 +89,8 @@ async def to_code(config):
|
||||
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
|
||||
)
|
||||
|
||||
# Although this component does not use SPI/Wire directly, the BSEC library requires them
|
||||
# Although this component does not use SPI, the BSEC library requires the SPI library
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library("Wire", None)
|
||||
|
||||
cg.add_define("USE_BSEC")
|
||||
cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480")
|
||||
|
||||
@@ -22,11 +22,17 @@ const LogString *cover_command_to_str(float pos) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN
|
||||
PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN");
|
||||
|
||||
const LogString *cover_operation_to_str(CoverOperation op) {
|
||||
return CoverOperationStrings::get_log_str(static_cast<uint8_t>(op), CoverOperationStrings::LAST_INDEX);
|
||||
switch (op) {
|
||||
case COVER_OPERATION_IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case COVER_OPERATION_OPENING:
|
||||
return LOG_STR("OPENING");
|
||||
case COVER_OPERATION_CLOSING:
|
||||
return LOG_STR("CLOSING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
Cover::Cover() : position{COVER_OPEN} {}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from esphome import yaml_util
|
||||
import esphome.codegen as cg
|
||||
@@ -51,7 +50,6 @@ from esphome.writer import clean_cmake_cache
|
||||
|
||||
from .boards import BOARDS, STANDARD_BOARDS
|
||||
from .const import ( # noqa
|
||||
KEY_ARDUINO_LIBRARIES,
|
||||
KEY_BOARD,
|
||||
KEY_COMPONENTS,
|
||||
KEY_ESP32,
|
||||
@@ -126,14 +124,10 @@ COMPILER_OPTIMIZATIONS = {
|
||||
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
|
||||
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
|
||||
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
|
||||
"esp_adc", # ADC driver - only needed by adc component
|
||||
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
|
||||
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
|
||||
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
|
||||
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
|
||||
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
||||
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
||||
"esp_eth", # Ethernet driver - only needed by ethernet component
|
||||
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
|
||||
"esp_http_client", # HTTP client - only needed by http_request component
|
||||
@@ -144,178 +138,14 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"espcoredump", # Core dump support - ESPHome has its own debug component
|
||||
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
|
||||
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
|
||||
"openthread", # Thread protocol - only needed by openthread component
|
||||
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
|
||||
"protocomm", # Protocol communication for provisioning - unused by ESPHome
|
||||
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
|
||||
"ulp", # ULP coprocessor - not currently used by any ESPHome component
|
||||
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
|
||||
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
|
||||
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
|
||||
)
|
||||
|
||||
# Additional IDF managed components to exclude for Arduino framework builds
|
||||
# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome
|
||||
# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because
|
||||
# that's how managed components are registered in the IDF build system
|
||||
# List includes direct dependencies from arduino-esp32/idf_component.yml
|
||||
# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need)
|
||||
ARDUINO_EXCLUDED_IDF_COMPONENTS = (
|
||||
"chmorgan__esp-libhelix-mp3", # MP3 decoder - not used
|
||||
"espressif__cbor", # CBOR library - only used by RainMaker/Insights
|
||||
"espressif__esp-dsp", # DSP library - not used
|
||||
"espressif__esp-modbus", # Modbus - ESPHome has its own
|
||||
"espressif__esp-sr", # Speech recognition - not used
|
||||
"espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used
|
||||
"espressif__esp-zigbee-lib", # Zigbee library - not used
|
||||
"espressif__esp_diag_data_store", # Diagnostics - not used
|
||||
"espressif__esp_diagnostics", # Diagnostics - not used
|
||||
"espressif__esp_hosted", # ESP hosted - only for ESP32-P4
|
||||
"espressif__esp_insights", # ESP Insights - not used
|
||||
"espressif__esp_modem", # Modem library - not used
|
||||
"espressif__esp_rainmaker", # RainMaker - not used
|
||||
"espressif__esp_rcp_update", # RCP update - RainMaker transitive dep
|
||||
"espressif__esp_schedule", # Schedule - RainMaker transitive dep
|
||||
"espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep
|
||||
"espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4
|
||||
"espressif__json_generator", # JSON generator - RainMaker transitive dep
|
||||
"espressif__json_parser", # JSON parser - RainMaker transitive dep
|
||||
"espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly
|
||||
"espressif__libsodium", # Crypto - ESPHome uses its own noise-c library
|
||||
"espressif__network_provisioning", # Network provisioning - not used
|
||||
"espressif__qrcode", # QR code - not used
|
||||
"espressif__rmaker_common", # RainMaker common - not used
|
||||
"joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem
|
||||
)
|
||||
|
||||
# Mapping of Arduino libraries to IDF managed components they require
|
||||
# When an Arduino library is enabled via cg.add_library(), these components
|
||||
# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS.
|
||||
#
|
||||
# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have
|
||||
# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle
|
||||
# their managed component dependencies. Our mapping is primarily needed for libraries
|
||||
# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.)
|
||||
# and to ensure the stubs are removed from our idf_component.yml overrides.
|
||||
ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = {
|
||||
"BLE": ("esp_driver_gptimer",),
|
||||
"BluetoothSerial": ("esp_driver_gptimer",),
|
||||
"ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"),
|
||||
"ESP_SR": ("espressif__esp-sr",),
|
||||
"Ethernet": ("espressif__lan867x",),
|
||||
"FFat": ("fatfs",),
|
||||
"Insights": (
|
||||
"espressif__cbor",
|
||||
"espressif__esp_insights",
|
||||
"espressif__esp_diagnostics",
|
||||
"espressif__esp_diag_data_store",
|
||||
"espressif__rmaker_common", # Transitive dep from esp_insights
|
||||
),
|
||||
"LittleFS": ("joltwallet__littlefs",),
|
||||
"Matter": ("espressif__esp_matter",),
|
||||
"PPP": ("espressif__esp_modem",),
|
||||
"RainMaker": (
|
||||
# Direct deps from idf_component.yml
|
||||
"espressif__cbor",
|
||||
"espressif__esp_rainmaker",
|
||||
"espressif__esp_insights",
|
||||
"espressif__esp_diagnostics",
|
||||
"espressif__esp_diag_data_store",
|
||||
"espressif__rmaker_common",
|
||||
"espressif__qrcode",
|
||||
# Transitive deps from esp_rainmaker
|
||||
"espressif__esp_rcp_update",
|
||||
"espressif__esp_schedule",
|
||||
"espressif__esp_secure_cert_mgr",
|
||||
"espressif__json_generator",
|
||||
"espressif__json_parser",
|
||||
"espressif__network_provisioning",
|
||||
),
|
||||
"SD": ("fatfs",),
|
||||
"SD_MMC": ("fatfs",),
|
||||
"SPIFFS": ("spiffs",),
|
||||
"WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"),
|
||||
"Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"),
|
||||
}
|
||||
|
||||
# Arduino library to Arduino library dependencies
|
||||
# When enabling one library, also enable its dependencies
|
||||
# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION
|
||||
ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = {
|
||||
"Ethernet": ("Network",),
|
||||
"WiFi": ("Network",),
|
||||
}
|
||||
|
||||
|
||||
def _idf_component_stub_name(component: str) -> str:
|
||||
"""Get stub directory name from IDF component name.
|
||||
|
||||
Component names are typically namespace__name (e.g., espressif__cbor).
|
||||
Returns just the name part (e.g., cbor). If no namespace is present,
|
||||
returns the original component name.
|
||||
"""
|
||||
_prefix, sep, suffix = component.partition("__")
|
||||
return suffix if sep else component
|
||||
|
||||
|
||||
def _idf_component_dep_name(component: str) -> str:
|
||||
"""Convert IDF component name to dependency format.
|
||||
|
||||
Converts espressif__cbor to espressif/cbor.
|
||||
"""
|
||||
return component.replace("__", "/")
|
||||
|
||||
|
||||
# Arduino libraries to disable by default when using Arduino framework
|
||||
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
|
||||
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
|
||||
# Components use cg.add_library() which auto-enables any they need
|
||||
# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt
|
||||
ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset(
|
||||
{
|
||||
"ArduinoOTA",
|
||||
"AsyncUDP",
|
||||
"BLE",
|
||||
"BluetoothSerial",
|
||||
"DNSServer",
|
||||
"EEPROM",
|
||||
"ESP_HostedOTA",
|
||||
"ESP_I2S",
|
||||
"ESP_NOW",
|
||||
"ESP_SR",
|
||||
"ESPmDNS",
|
||||
"Ethernet",
|
||||
"FFat",
|
||||
"FS",
|
||||
"Hash",
|
||||
"HTTPClient",
|
||||
"HTTPUpdate",
|
||||
"Insights",
|
||||
"LittleFS",
|
||||
"Matter",
|
||||
"NetBIOS",
|
||||
"Network",
|
||||
"NetworkClientSecure",
|
||||
"OpenThread",
|
||||
"PPP",
|
||||
"Preferences",
|
||||
"RainMaker",
|
||||
"SD",
|
||||
"SD_MMC",
|
||||
"SimpleBLE",
|
||||
"SPI",
|
||||
"SPIFFS",
|
||||
"Ticker",
|
||||
"Update",
|
||||
"USB",
|
||||
"WebServer",
|
||||
"WiFi",
|
||||
"WiFiProv",
|
||||
"Wire",
|
||||
"Zigbee",
|
||||
}
|
||||
)
|
||||
|
||||
# ESP32 (original) chip revision options
|
||||
# Setting minimum revision to 3.0 or higher:
|
||||
# - Reduces flash size by excluding workaround code for older chip bugs
|
||||
@@ -407,13 +237,7 @@ def set_core_data(config):
|
||||
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
|
||||
# Initialize with default exclusions - components can call include_builtin_idf_component()
|
||||
# to re-enable any they need
|
||||
excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
|
||||
# Add Arduino-specific managed component exclusions when using Arduino framework
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||
excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS)
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded
|
||||
# Initialize Arduino library tracking - cg.add_library() auto-enables libraries
|
||||
CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set()
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
|
||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
|
||||
config[CONF_FRAMEWORK][CONF_VERSION]
|
||||
)
|
||||
@@ -561,26 +385,6 @@ def include_builtin_idf_component(name: str) -> None:
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name)
|
||||
|
||||
|
||||
def _enable_arduino_library(name: str) -> None:
|
||||
"""Enable an Arduino library that is disabled by default.
|
||||
|
||||
This is called automatically by CORE.add_library() when a component adds
|
||||
an Arduino library via cg.add_library(). Components should not call this
|
||||
directly - just use cg.add_library("LibName", None).
|
||||
|
||||
Args:
|
||||
name: The library name (e.g., "Wire", "SPI", "WiFi")
|
||||
"""
|
||||
enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES]
|
||||
enabled_libs.add(name)
|
||||
# Also enable any required Arduino library dependencies
|
||||
for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()):
|
||||
enabled_libs.add(dep_lib)
|
||||
# Also enable any required IDF components
|
||||
for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()):
|
||||
include_builtin_idf_component(idf_component)
|
||||
|
||||
|
||||
def add_extra_script(stage: str, filename: str, path: Path):
|
||||
"""Add an extra script to the project."""
|
||||
key = f"{stage}:{filename}"
|
||||
@@ -1322,27 +1126,6 @@ async def _write_exclude_components() -> None:
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _write_arduino_libraries_sdkconfig() -> None:
|
||||
"""Write Arduino selective compilation sdkconfig after all components have added libraries.
|
||||
|
||||
This must run at FINAL priority so that all components have had a chance to call
|
||||
cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library().
|
||||
"""
|
||||
if KEY_ESP32 not in CORE.data:
|
||||
return
|
||||
# Enable Arduino selective compilation to disable unused Arduino libraries
|
||||
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
|
||||
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
|
||||
# cg.add_library() auto-enables needed libraries; users can also add
|
||||
# libraries via esphome: libraries: config which calls cg.add_library()
|
||||
add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True)
|
||||
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
|
||||
for lib in ARDUINO_DISABLED_LIBRARIES:
|
||||
# Enable if explicitly requested, disable otherwise
|
||||
add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_yaml_idf_components(components: list[ConfigType]):
|
||||
"""Add IDF components from YAML config with final priority to override code-added components."""
|
||||
@@ -1761,11 +1544,6 @@ async def to_code(config):
|
||||
# Default exclusions are added in set_core_data() during config validation.
|
||||
CORE.add_job(_write_exclude_components)
|
||||
|
||||
# Write Arduino selective compilation sdkconfig at FINAL priority after all
|
||||
# components have had a chance to call cg.add_library() to enable libraries they need.
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||
CORE.add_job(_write_arduino_libraries_sdkconfig)
|
||||
|
||||
|
||||
APP_PARTITION_SIZES = {
|
||||
"2MB": 0x0C0000, # 768 KB
|
||||
@@ -1846,49 +1624,11 @@ def _write_sdkconfig():
|
||||
|
||||
def _write_idf_component_yml():
|
||||
yml_path = CORE.relative_build_path("src/idf_component.yml")
|
||||
dependencies: dict[str, dict] = {}
|
||||
|
||||
# For Arduino builds, override unused managed components from the Arduino framework
|
||||
# by pointing them to empty stub directories using override_path
|
||||
# This prevents the IDF component manager from downloading the real components
|
||||
if CORE.using_arduino:
|
||||
# Determine which IDF components are needed by enabled Arduino libraries
|
||||
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
|
||||
required_idf_components = {
|
||||
comp
|
||||
for lib in enabled_libs
|
||||
for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ())
|
||||
}
|
||||
|
||||
# Only stub components that are not required by any enabled Arduino library
|
||||
components_to_stub = (
|
||||
set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components
|
||||
)
|
||||
|
||||
stubs_dir = CORE.relative_build_path("component_stubs")
|
||||
stubs_dir.mkdir(exist_ok=True)
|
||||
for component_name in components_to_stub:
|
||||
# Create stub directory with minimal CMakeLists.txt
|
||||
stub_path = stubs_dir / _idf_component_stub_name(component_name)
|
||||
stub_path.mkdir(exist_ok=True)
|
||||
stub_cmake = stub_path / "CMakeLists.txt"
|
||||
if not stub_cmake.exists():
|
||||
stub_cmake.write_text("idf_component_register()\n")
|
||||
dependencies[_idf_component_dep_name(component_name)] = {
|
||||
"version": "*",
|
||||
"override_path": str(stub_path),
|
||||
}
|
||||
|
||||
# Remove stubs for components that are now required by enabled libraries
|
||||
for component_name in required_idf_components:
|
||||
stub_path = stubs_dir / _idf_component_stub_name(component_name)
|
||||
if stub_path.exists():
|
||||
shutil.rmtree(stub_path)
|
||||
|
||||
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
|
||||
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
|
||||
dependencies = {}
|
||||
for name, component in components.items():
|
||||
dependency: dict[str, str] = {}
|
||||
dependency = {}
|
||||
if component[KEY_REF]:
|
||||
dependency["version"] = component[KEY_REF]
|
||||
if component[KEY_REPO]:
|
||||
@@ -1896,8 +1636,9 @@ def _write_idf_component_yml():
|
||||
if component[KEY_PATH]:
|
||||
dependency["path"] = component[KEY_PATH]
|
||||
dependencies[name] = dependency
|
||||
|
||||
contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else ""
|
||||
contents = yaml_util.dump({"dependencies": dependencies})
|
||||
else:
|
||||
contents = ""
|
||||
if write_file_if_changed(yml_path, contents):
|
||||
dependencies_lock = CORE.relative_build_path("dependencies.lock")
|
||||
if dependencies_lock.is_file():
|
||||
|
||||
@@ -7,7 +7,6 @@ KEY_VARIANT = "variant"
|
||||
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||
KEY_COMPONENTS = "components"
|
||||
KEY_EXCLUDE_COMPONENTS = "exclude_components"
|
||||
KEY_ARDUINO_LIBRARIES = "arduino_libraries"
|
||||
KEY_REPO = "repo"
|
||||
KEY_REF = "ref"
|
||||
KEY_REFRESH = "refresh"
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
|
||||
@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
@@ -169,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
if (actual_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
@@ -179,7 +188,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -98,10 +98,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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -48,7 +48,7 @@ class ESPBTUUID {
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
std::string to_string() const; // NOLINT
|
||||
std::string to_string() const;
|
||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,7 +15,6 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -122,10 +121,6 @@ def get_default_tx_enqueue_timeout(bit_rate):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Legacy driver component provides driver/twai.h header
|
||||
include_builtin_idf_component("driver")
|
||||
# Also enable esp_driver_twai for future migration to new API
|
||||
include_builtin_idf_component("esp_driver_twai")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await canbus.register_canbus(var, config)
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S2,
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
|
||||
|
||||
@@ -43,7 +38,6 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
include_builtin_idf_component("esp_driver_dac")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await output.register_output(var, config)
|
||||
|
||||
@@ -34,29 +34,14 @@ static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_M
|
||||
ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1);
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// Parse an integer from str, advancing ptr past the number
|
||||
// Returns false if no digits were parsed
|
||||
static bool parse_int(const char *&ptr, int &value) {
|
||||
char *end;
|
||||
value = static_cast<int>(strtol(ptr, &end, 10));
|
||||
if (end == ptr)
|
||||
return false;
|
||||
ptr = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse version string "major.minor.patch" into components
|
||||
// Returns true if at least major.minor was parsed
|
||||
// Returns true if parsing succeeded
|
||||
static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) {
|
||||
major = minor = patch = 0;
|
||||
const char *ptr = version_str.c_str();
|
||||
|
||||
if (!parse_int(ptr, major) || *ptr++ != '.' || !parse_int(ptr, minor))
|
||||
return false;
|
||||
if (*ptr == '.')
|
||||
parse_int(++ptr, patch);
|
||||
|
||||
return true;
|
||||
if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare two versions, returns:
|
||||
@@ -211,14 +196,11 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
int read_or_error = container->read(buf, sizeof(buf));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result =
|
||||
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||
// but this is defensive code in case chunked transfer encoding support is added in the future.
|
||||
if (result != http_request::HttpReadLoopResult::DATA)
|
||||
break; // COMPLETE, ERROR, or TIMEOUT
|
||||
break; // ERROR or TIMEOUT
|
||||
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
|
||||
}
|
||||
container->end();
|
||||
@@ -339,14 +321,9 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result =
|
||||
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||
// but this is defensive code in case chunked transfer encoding support is added in the future.
|
||||
if (result == http_request::HttpReadLoopResult::COMPLETE)
|
||||
break;
|
||||
if (result != http_request::HttpReadLoopResult::DATA) {
|
||||
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading firmware data");
|
||||
|
||||
@@ -269,8 +269,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_driver_touch_sens")
|
||||
# Legacy driver component provides driver/touch_sensor.h header
|
||||
include_builtin_idf_component("driver")
|
||||
|
||||
touch = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(touch, config)
|
||||
|
||||
@@ -13,7 +13,7 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WIFI,
|
||||
)
|
||||
from esphome.core import HexInt
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
@@ -124,6 +124,9 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
|
||||
# This enables low-latency event processing instead of waiting for select() timeout
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
@@ -427,6 +427,9 @@ async def to_code(config):
|
||||
# Add LAN867x 10BASE-T1S PHY support component
|
||||
add_idf_component(name="espressif/lan867x", ref="2.0.0")
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
CORE.add_job(final_step)
|
||||
|
||||
|
||||
|
||||
@@ -802,8 +802,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
|
||||
/*
|
||||
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
|
||||
@@ -820,10 +820,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
|
||||
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -26,7 +26,6 @@ struct Header {
|
||||
enum HttpStatus {
|
||||
HTTP_STATUS_OK = 200,
|
||||
HTTP_STATUS_NO_CONTENT = 204,
|
||||
HTTP_STATUS_RESET_CONTENT = 205,
|
||||
HTTP_STATUS_PARTIAL_CONTENT = 206,
|
||||
|
||||
/* 3xx - Redirection */
|
||||
@@ -127,21 +126,19 @@ struct HttpReadResult {
|
||||
|
||||
/// Result of processing a non-blocking read with timeout (for manual loops)
|
||||
enum class HttpReadLoopResult : uint8_t {
|
||||
DATA, ///< Data was read, process it
|
||||
COMPLETE, ///< All content has been read, caller should exit loop
|
||||
RETRY, ///< No data yet, already delayed, caller should continue loop
|
||||
ERROR, ///< Read error, caller should exit loop
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
DATA, ///< Data was read, process it
|
||||
RETRY, ///< No data yet, already delayed, caller should continue loop
|
||||
ERROR, ///< Read error, caller should exit loop
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
};
|
||||
|
||||
/// Process a read result with timeout tracking and delay handling
|
||||
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
||||
/// @param last_data_time Time of last successful read, updated when data received
|
||||
/// @param timeout_ms Maximum time to wait for data
|
||||
/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete())
|
||||
/// @return How the caller should proceed - see HttpReadLoopResult enum
|
||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms,
|
||||
bool is_read_complete) {
|
||||
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
|
||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
|
||||
uint32_t timeout_ms) {
|
||||
if (bytes_read_or_error > 0) {
|
||||
last_data_time = millis();
|
||||
return HttpReadLoopResult::DATA;
|
||||
@@ -149,10 +146,7 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_
|
||||
if (bytes_read_or_error < 0) {
|
||||
return HttpReadLoopResult::ERROR;
|
||||
}
|
||||
// bytes_read_or_error == 0: either "no data yet" or "all content read"
|
||||
if (is_read_complete) {
|
||||
return HttpReadLoopResult::COMPLETE;
|
||||
}
|
||||
// bytes_read_or_error == 0: no data available yet
|
||||
if (millis() - last_data_time >= timeout_ms) {
|
||||
return HttpReadLoopResult::TIMEOUT;
|
||||
}
|
||||
@@ -165,9 +159,9 @@ class HttpRequestComponent;
|
||||
class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
public:
|
||||
virtual ~HttpContainer() = default;
|
||||
size_t content_length{0};
|
||||
int status_code{-1}; ///< -1 indicates no response received yet
|
||||
uint32_t duration_ms{0};
|
||||
size_t content_length;
|
||||
int status_code;
|
||||
uint32_t duration_ms;
|
||||
|
||||
/**
|
||||
* @brief Read data from the HTTP response body.
|
||||
@@ -200,24 +194,9 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
virtual void end() = 0;
|
||||
|
||||
void set_secure(bool secure) { this->secure_ = secure; }
|
||||
void set_chunked(bool chunked) { this->is_chunked_ = chunked; }
|
||||
|
||||
size_t get_bytes_read() const { return this->bytes_read_; }
|
||||
|
||||
/// Check if all expected content has been read
|
||||
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
|
||||
bool is_read_complete() const {
|
||||
// Per RFC 9112, these responses have no body:
|
||||
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
|
||||
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
|
||||
this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) {
|
||||
return true;
|
||||
}
|
||||
// For non-chunked responses, complete when bytes_read >= content_length
|
||||
// This handles both Content-Length: 0 and Content-Length: N cases
|
||||
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get response headers.
|
||||
*
|
||||
@@ -230,7 +209,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
protected:
|
||||
size_t bytes_read_{0};
|
||||
bool secure_{false};
|
||||
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
|
||||
std::map<std::string, std::list<std::string>> response_headers_{};
|
||||
};
|
||||
|
||||
@@ -241,7 +219,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
/// @param total_size Total bytes to read
|
||||
/// @param chunk_size Maximum bytes per read call
|
||||
/// @param timeout_ms Read timeout in milliseconds
|
||||
/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read
|
||||
/// @return HttpReadResult with status and error_code on failure
|
||||
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
|
||||
uint32_t timeout_ms) {
|
||||
size_t read_index = 0;
|
||||
@@ -253,11 +231,9 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer,
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete());
|
||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result == HttpReadLoopResult::COMPLETE)
|
||||
break; // Server sent less data than requested, but transfer is complete
|
||||
if (result == HttpReadLoopResult::ERROR)
|
||||
return {HttpReadStatus::ERROR, read_bytes_or_error};
|
||||
if (result == HttpReadLoopResult::TIMEOUT)
|
||||
@@ -417,12 +393,11 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result =
|
||||
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA)
|
||||
break; // COMPLETE, ERROR, or TIMEOUT
|
||||
break; // ERROR or TIMEOUT
|
||||
read_index += read_or_error;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
|
||||
@@ -135,23 +135,9 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
|
||||
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
|
||||
// early return check (bytes_read_ >= content_length) will never trigger.
|
||||
//
|
||||
// TODO: Chunked transfer encoding is NOT properly supported on Arduino.
|
||||
// The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where
|
||||
// esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr()
|
||||
// returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead
|
||||
// of decoded content. This wasn't noticed because requests would complete and payloads
|
||||
// were only examined on IDF. The long transfer times were also masked by the misleading
|
||||
// "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues:
|
||||
// 1. Response body is corrupted - contains chunk size headers mixed with data
|
||||
// 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout
|
||||
// The proper fix would be to use getString() for chunked responses, which decodes chunks
|
||||
// internally, but this buffers the entire response in memory.
|
||||
int content_length = container->client_.getSize();
|
||||
ESP_LOGD(TAG, "Content-Length: %d", content_length);
|
||||
container->content_length = (size_t) content_length;
|
||||
// -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding
|
||||
container->set_chunked(content_length == -1);
|
||||
container->duration_ms = millis() - start;
|
||||
|
||||
return container;
|
||||
@@ -192,9 +178,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
// Check if we've read all expected content (non-chunked only)
|
||||
// For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
|
||||
if (this->is_read_complete()) {
|
||||
// Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX)
|
||||
// For chunked encoding (content_length == SIZE_MAX), we can't use this check
|
||||
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
|
||||
@@ -160,7 +160,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
|
||||
// The read() method handles content_length == 0 specially to support chunked responses.
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
@@ -196,7 +195,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
|
||||
container->feed_wdt();
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
@@ -241,9 +239,10 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content (non-chunked only)
|
||||
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
|
||||
if (this->is_read_complete()) {
|
||||
// Check if we've already read all expected content
|
||||
// Skip this check when content_length is 0 (chunked transfer encoding or unknown length)
|
||||
// For chunked responses, esp_http_client_read() will return 0 when all data is received
|
||||
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
|
||||
|
||||
@@ -130,13 +130,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||
// but this is defensive code in case chunked transfer encoding support is added for OTA in the future.
|
||||
if (result == HttpReadLoopResult::COMPLETE)
|
||||
break;
|
||||
if (result != HttpReadLoopResult::DATA) {
|
||||
if (result == HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading data");
|
||||
|
||||
@@ -90,14 +90,16 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
size_t content_length = container->content_length;
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = false;
|
||||
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
std::string response((char *) data, read_index);
|
||||
allocator.deallocate(data, container->content_length);
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
@@ -135,7 +137,6 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
allocator.deallocate(data, content_length);
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
@@ -156,12 +157,17 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
}
|
||||
}
|
||||
|
||||
{ // Ensures the current version string falls out of scope and deallocates before the task ends
|
||||
std::string current_version;
|
||||
#ifdef ESPHOME_PROJECT_VERSION
|
||||
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
|
||||
current_version = ESPHOME_PROJECT_VERSION;
|
||||
#else
|
||||
this_update->update_info_.current_version = ESPHOME_VERSION;
|
||||
current_version = ESPHOME_VERSION;
|
||||
#endif
|
||||
|
||||
this_update->update_info_.current_version = current_version;
|
||||
}
|
||||
|
||||
bool trigger_update_available = false;
|
||||
|
||||
if (this_update->update_info_.latest_version.empty() ||
|
||||
|
||||
@@ -119,7 +119,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,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,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
@@ -286,9 +285,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]))
|
||||
|
||||
@@ -114,7 +114,6 @@ async def to_code(config):
|
||||
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
|
||||
cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb"))
|
||||
|
||||
cg.add_library("WiFi", None)
|
||||
cg.add_library("NetworkClientSecure", None)
|
||||
cg.add_library("HTTPClient", None)
|
||||
cg.add_library("esphome/ESP32-audioI2S", "2.3.0")
|
||||
|
||||
@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
for (auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
const char *ssid_cstr = scan.get_ssid().c_str();
|
||||
// Check if we've already sent this SSID
|
||||
bool duplicate = false;
|
||||
for (const auto &seen : networks) {
|
||||
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate)
|
||||
const std::string &ssid = scan.get_ssid();
|
||||
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
|
||||
continue;
|
||||
// Only allocate std::string after confirming it's not a duplicate
|
||||
std::string ssid(ssid_cstr);
|
||||
// Send each ssid separately to avoid overflowing the buffer
|
||||
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
|
||||
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
|
||||
std::vector<uint8_t> data =
|
||||
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
|
||||
this->send_response_(data);
|
||||
networks.push_back(std::move(ssid));
|
||||
networks.push_back(ssid);
|
||||
}
|
||||
// Send empty response to signify the end of the list.
|
||||
std::vector<uint8_t> data =
|
||||
|
||||
@@ -15,7 +15,7 @@ static const char *const TAG = "json";
|
||||
static SpiRamAllocator global_json_allocator;
|
||||
#endif
|
||||
|
||||
SerializationBuffer<> build_json(const json_build_t &f) {
|
||||
std::string build_json(const json_build_t &f) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
@@ -25,13 +25,8 @@ SerializationBuffer<> build_json(const json_build_t &f) {
|
||||
}
|
||||
|
||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
|
||||
}
|
||||
|
||||
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonDocument doc = parse_json(data, len);
|
||||
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
|
||||
if (doc.overflowed() || doc.isNull())
|
||||
return false;
|
||||
return f(doc.as<JsonObject>());
|
||||
@@ -66,62 +61,14 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
SerializationBuffer<> JsonBuilder::serialize() {
|
||||
// ===========================================================================================
|
||||
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
|
||||
// ===========================================================================================
|
||||
//
|
||||
// This function is carefully structured to enable NRVO. The compiler constructs `result`
|
||||
// directly in the caller's stack frame, eliminating the move constructor call entirely.
|
||||
//
|
||||
// WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which
|
||||
// must memcpy up to 768 bytes of stack buffer content. This happens on EVERY JSON
|
||||
// serialization (sensor updates, web server responses, MQTT publishes, etc.).
|
||||
//
|
||||
// WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly
|
||||
// where the caller needs it.
|
||||
//
|
||||
// Requirements for NRVO to work:
|
||||
// 1. Single named variable (`result`) returned from ALL paths
|
||||
// 2. All paths must return the SAME variable (not different variables)
|
||||
// 3. No std::move() on the return statement
|
||||
//
|
||||
// If you must modify this function:
|
||||
// - Keep a single `result` variable declared at the top
|
||||
// - All code paths must return `result` (not a different variable)
|
||||
// - Verify NRVO still works by checking the disassembly for move constructor calls
|
||||
// - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer"
|
||||
// Should show only destructor, NOT move constructor
|
||||
//
|
||||
// Why we avoid measureJson(): It instantiates DummyWriter templates adding ~1KB flash.
|
||||
// Instead, try stack buffer first. 768 bytes covers 99.9% of JSON payloads (sensors ~200B,
|
||||
// lights ~170B, climate ~700B). Only entities with 40+ options exceed this.
|
||||
//
|
||||
// ===========================================================================================
|
||||
constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
|
||||
SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
|
||||
|
||||
std::string JsonBuilder::serialize() {
|
||||
if (doc_.overflowed()) {
|
||||
ESP_LOGE(TAG, "JSON document overflow");
|
||||
auto *buf = result.data_writable_();
|
||||
buf[0] = '{';
|
||||
buf[1] = '}';
|
||||
buf[2] = '\0';
|
||||
result.set_size_(2);
|
||||
return result;
|
||||
return "{}";
|
||||
}
|
||||
|
||||
size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
|
||||
if (size < buf_size) {
|
||||
// Fits in stack buffer - update size to actual length
|
||||
result.set_size_(size);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Needs heap allocation - reallocate and serialize again with exact size
|
||||
result.reallocate_heap_(size);
|
||||
serializeJson(doc_, result.data_writable_(), size + 1);
|
||||
return result;
|
||||
std::string output;
|
||||
serializeJson(doc_, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace json
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
@@ -16,108 +14,6 @@
|
||||
namespace esphome {
|
||||
namespace json {
|
||||
|
||||
/// Buffer for JSON serialization that uses stack allocation for small payloads.
|
||||
/// Template parameter STACK_SIZE specifies the stack buffer size (default 768 bytes).
|
||||
/// Supports move semantics for efficient return-by-value.
|
||||
template<size_t STACK_SIZE = 768> class SerializationBuffer {
|
||||
public:
|
||||
static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation
|
||||
|
||||
/// Construct with known size (typically from measureJson)
|
||||
explicit SerializationBuffer(size_t size) : size_(size) {
|
||||
if (size + 1 <= STACK_SIZE) {
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
heap_buffer_ = new char[size + 1];
|
||||
buffer_ = heap_buffer_;
|
||||
}
|
||||
buffer_[0] = '\0';
|
||||
}
|
||||
|
||||
~SerializationBuffer() { delete[] heap_buffer_; }
|
||||
|
||||
// Move constructor - works with same template instantiation
|
||||
SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) {
|
||||
if (other.buffer_ == other.stack_buffer_) {
|
||||
// Stack buffer - must copy content
|
||||
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
// Heap buffer - steal ownership
|
||||
buffer_ = heap_buffer_;
|
||||
other.heap_buffer_ = nullptr;
|
||||
}
|
||||
// Leave moved-from object in valid empty state
|
||||
other.stack_buffer_[0] = '\0';
|
||||
other.buffer_ = other.stack_buffer_;
|
||||
other.size_ = 0;
|
||||
}
|
||||
|
||||
// Move assignment
|
||||
SerializationBuffer &operator=(SerializationBuffer &&other) noexcept {
|
||||
if (this != &other) {
|
||||
delete[] heap_buffer_;
|
||||
heap_buffer_ = other.heap_buffer_;
|
||||
size_ = other.size_;
|
||||
if (other.buffer_ == other.stack_buffer_) {
|
||||
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
buffer_ = heap_buffer_;
|
||||
other.heap_buffer_ = nullptr;
|
||||
}
|
||||
// Leave moved-from object in valid empty state
|
||||
other.stack_buffer_[0] = '\0';
|
||||
other.buffer_ = other.stack_buffer_;
|
||||
other.size_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Delete copy operations
|
||||
SerializationBuffer(const SerializationBuffer &) = delete;
|
||||
SerializationBuffer &operator=(const SerializationBuffer &) = delete;
|
||||
|
||||
/// Get null-terminated C string
|
||||
const char *c_str() const { return buffer_; }
|
||||
/// Get data pointer
|
||||
const char *data() const { return buffer_; }
|
||||
/// Get string length (excluding null terminator)
|
||||
size_t size() const { return size_; }
|
||||
|
||||
/// Implicit conversion to std::string for backward compatibility
|
||||
/// WARNING: This allocates a new std::string on the heap. Prefer using
|
||||
/// c_str() or data()/size() directly when possible to avoid allocation.
|
||||
operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor)
|
||||
|
||||
private:
|
||||
friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods
|
||||
|
||||
/// Get writable buffer (for serialization)
|
||||
char *data_writable_() { return buffer_; }
|
||||
/// Set actual size after serialization (must not exceed allocated size)
|
||||
/// Also ensures null termination for c_str() safety
|
||||
void set_size_(size_t size) {
|
||||
size_ = size;
|
||||
buffer_[size] = '\0';
|
||||
}
|
||||
|
||||
/// Reallocate to heap buffer with new size (for when stack buffer is too small)
|
||||
/// This invalidates any previous buffer content. Used by JsonBuilder::serialize().
|
||||
void reallocate_heap_(size_t size) {
|
||||
delete[] heap_buffer_;
|
||||
heap_buffer_ = new char[size + 1];
|
||||
buffer_ = heap_buffer_;
|
||||
size_ = size;
|
||||
buffer_[0] = '\0';
|
||||
}
|
||||
|
||||
char stack_buffer_[STACK_SIZE];
|
||||
char *heap_buffer_{nullptr};
|
||||
char *buffer_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
#ifdef USE_PSRAM
|
||||
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||
// This is only compiled when PSRAM is enabled
|
||||
@@ -150,13 +46,10 @@ using json_parse_t = std::function<bool(JsonObject)>;
|
||||
using json_build_t = std::function<void(JsonObject)>;
|
||||
|
||||
/// Build a JSON string with the provided json build function.
|
||||
/// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string.
|
||||
SerializationBuffer<> build_json(const json_build_t &f);
|
||||
std::string build_json(const json_build_t &f);
|
||||
|
||||
/// Parse a JSON string and run the provided json parse function if it's valid.
|
||||
bool parse_json(const std::string &data, const json_parse_t &f);
|
||||
/// Parse JSON from raw bytes and run the provided json parse function if it's valid.
|
||||
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f);
|
||||
|
||||
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
|
||||
JsonDocument parse_json(const uint8_t *data, size_t len);
|
||||
@@ -176,9 +69,7 @@ class JsonBuilder {
|
||||
return root_;
|
||||
}
|
||||
|
||||
/// Serialize the JSON document to a SerializationBuffer (stack-first allocation)
|
||||
/// Uses 768-byte stack buffer by default, falls back to heap for larger JSON
|
||||
SerializationBuffer<> serialize();
|
||||
std::string serialize();
|
||||
|
||||
private:
|
||||
#ifdef USE_PSRAM
|
||||
|
||||
@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
|
||||
static const uint8_t CMD_READ_REG = 0x03;
|
||||
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
|
||||
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
|
||||
@@ -18,7 +18,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -117,11 +126,11 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
if (this->is_changed_(&this->db, save, key_str)) {
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
fdb_blob_make(&this->blob, save.data.get(), save.len);
|
||||
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
|
||||
if (err != FDB_NO_ERR) {
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -129,7 +138,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
@@ -153,7 +162,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Check size first - if different, data has changed
|
||||
if (kv.value_len != to_save.data.size()) {
|
||||
if (kv.value_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -167,7 +176,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Compare the actual data
|
||||
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -9,19 +9,32 @@ namespace esphome::light {
|
||||
|
||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||
|
||||
// Color mode JSON strings - packed into flash with compile-time generated offsets.
|
||||
// Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff").
|
||||
PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct",
|
||||
"rgbww");
|
||||
|
||||
// Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0).
|
||||
// Returns ProgmemStr so ArduinoJson knows to handle PROGMEM strings on ESP8266.
|
||||
// Get JSON string for color mode.
|
||||
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would
|
||||
// generate a large jump table. Converting to bit index (0-9) allows a compact switch.
|
||||
static ProgmemStr get_color_mode_json_str(ColorMode mode) {
|
||||
unsigned bit = ColorModeBitPolicy::to_bit(mode);
|
||||
if (bit == 0)
|
||||
return nullptr;
|
||||
// bit is 1-9 for valid modes, so bit-1 is always valid (0-8). LAST_INDEX fallback never used.
|
||||
return ColorModeStrings::get_progmem_str(bit - 1, ColorModeStrings::LAST_INDEX);
|
||||
switch (ColorModeBitPolicy::to_bit(mode)) {
|
||||
case 1:
|
||||
return ESPHOME_F("onoff");
|
||||
case 2:
|
||||
return ESPHOME_F("brightness");
|
||||
case 3:
|
||||
return ESPHOME_F("white");
|
||||
case 4:
|
||||
return ESPHOME_F("color_temp");
|
||||
case 5:
|
||||
return ESPHOME_F("cwww");
|
||||
case 6:
|
||||
return ESPHOME_F("rgb");
|
||||
case 7:
|
||||
return ESPHOME_F("rgbw");
|
||||
case 8:
|
||||
return ESPHOME_F("rgbct");
|
||||
case 9:
|
||||
return ESPHOME_F("rgbww");
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
@@ -292,20 +291,34 @@ UARTSelection Logger::get_uart() const { return this->uart_; }
|
||||
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
// Log level strings - packed into flash on ESP8266, indexed by log level (0-7)
|
||||
PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE");
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// ESP8266: PSTR() cannot be used in array initializers, so we need to declare
|
||||
// each string separately as a global constant first
|
||||
static const char LOG_LEVEL_NONE[] PROGMEM = "NONE";
|
||||
static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR";
|
||||
static const char LOG_LEVEL_WARN[] PROGMEM = "WARN";
|
||||
static const char LOG_LEVEL_INFO[] PROGMEM = "INFO";
|
||||
static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG";
|
||||
static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG";
|
||||
static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE";
|
||||
static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE";
|
||||
|
||||
static const LogString *get_log_level_str(uint8_t level) {
|
||||
return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX);
|
||||
}
|
||||
static const LogString *const LOG_LEVELS[] = {
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_NONE), reinterpret_cast<const LogString *>(LOG_LEVEL_ERROR),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_WARN), reinterpret_cast<const LogString *>(LOG_LEVEL_INFO),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_CONFIG), reinterpret_cast<const LogString *>(LOG_LEVEL_DEBUG),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_VERBOSE), reinterpret_cast<const LogString *>(LOG_LEVEL_VERY_VERBOSE),
|
||||
};
|
||||
#else
|
||||
static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
|
||||
#endif
|
||||
|
||||
void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Logger:\n"
|
||||
" Max Level: %s\n"
|
||||
" Initial Level: %s",
|
||||
LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)),
|
||||
LOG_STR_ARG(get_log_level_str(this->current_level_)));
|
||||
LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_]));
|
||||
#ifndef USE_HOST
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Log Baud Rate: %" PRIu32 "\n"
|
||||
@@ -324,7 +337,7 @@ void Logger::dump_config() {
|
||||
|
||||
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
for (auto &it : this->log_levels_) {
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second)));
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second]));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -332,8 +345,7 @@ void Logger::dump_config() {
|
||||
void Logger::set_log_level(uint8_t level) {
|
||||
if (level > ESPHOME_LOG_LEVEL) {
|
||||
level = ESPHOME_LOG_LEVEL;
|
||||
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s",
|
||||
LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)));
|
||||
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]));
|
||||
}
|
||||
this->current_level_ = level;
|
||||
#ifdef USE_LOGGER_LEVEL_LISTENERS
|
||||
|
||||
@@ -234,7 +234,6 @@ class Logger : public Component {
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
// RAII guard for recursion flags - sets flag on construction, clears on destruction
|
||||
class RecursionGuard {
|
||||
public:
|
||||
@@ -261,6 +260,7 @@ class Logger : public Component {
|
||||
#endif
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// It's the caller's responsibility to initialize buffer_at (typically to 0)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#ifdef USE_ESP8266
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
Serial.swap();
|
||||
}
|
||||
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
uart_set_debug(UART_NO);
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
default:
|
||||
return LOG_STR("UART0_SWAP");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif
|
||||
@@ -1,22 +0,0 @@
|
||||
#if defined(USE_HOST)
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
|
||||
fputs(buffer, stdout);
|
||||
puts(msg);
|
||||
}
|
||||
|
||||
void Logger::pre_setup() { global_logger = this; }
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -1,70 +0,0 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
#if LT_HW_UART0
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial0;
|
||||
Serial0.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART1
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART2
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ != UART_SELECTION_DEFAULT) {
|
||||
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
|
||||
"The default port was used instead.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// change lt_log() port to match default Serial
|
||||
if (this->uart_ == UART_SELECTION_DEFAULT) {
|
||||
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
|
||||
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
|
||||
} else {
|
||||
lt_log_set_port(this->uart_ - 1);
|
||||
}
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_DEFAULT:
|
||||
return LOG_STR("DEFAULT");
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART2:
|
||||
default:
|
||||
return LOG_STR("UART2");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -1,48 +0,0 @@
|
||||
#ifdef USE_RP2040
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_USB_CDC:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif // USE_RP2040
|
||||
@@ -1,96 +0,0 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/usb/usb_device.h>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
void Logger::loop() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
static bool opened = false;
|
||||
uint32_t dtr = 0;
|
||||
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
|
||||
|
||||
/* Poll if the DTR flag was set, optional */
|
||||
if (opened == dtr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opened) {
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
opened = !opened;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
static const struct device *uart_dev = nullptr;
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
|
||||
break;
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||
if (device_is_ready(uart_dev)) {
|
||||
usb_enable(nullptr);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
if (!device_is_ready(uart_dev)) {
|
||||
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
|
||||
} else {
|
||||
this->uart_dev_ = uart_dev;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
if (nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
while (*msg) {
|
||||
uart_poll_out(this->uart_dev_, *msg);
|
||||
++msg;
|
||||
}
|
||||
uart_poll_out(this->uart_dev_, '\n');
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ from esphome.const import (
|
||||
UNIT_PERCENT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.core import CORE, coroutine
|
||||
from esphome.core import coroutine
|
||||
|
||||
CODEOWNERS = ["@dudanov"]
|
||||
DEPENDENCIES = ["climate", "uart"]
|
||||
@@ -290,7 +290,4 @@ async def to_code(config):
|
||||
if CONF_HUMIDITY_SETPOINT in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
|
||||
cg.add(var.set_humidity_setpoint_sensor(sens))
|
||||
# MideaUART library requires WiFi (WiFi auto-enables Network via dependency mapping)
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("WiFi", None)
|
||||
cg.add_library("dudanov/MideaUART", "1.1.9")
|
||||
|
||||
@@ -13,33 +13,6 @@ static const char *const TAG = "mqtt.alarm_control_panel";
|
||||
|
||||
using namespace esphome::alarm_control_panel;
|
||||
|
||||
static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) {
|
||||
switch (state) {
|
||||
case ACP_STATE_DISARMED:
|
||||
return ESPHOME_F("disarmed");
|
||||
case ACP_STATE_ARMED_HOME:
|
||||
return ESPHOME_F("armed_home");
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
return ESPHOME_F("armed_away");
|
||||
case ACP_STATE_ARMED_NIGHT:
|
||||
return ESPHOME_F("armed_night");
|
||||
case ACP_STATE_ARMED_VACATION:
|
||||
return ESPHOME_F("armed_vacation");
|
||||
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||
return ESPHOME_F("armed_custom_bypass");
|
||||
case ACP_STATE_PENDING:
|
||||
return ESPHOME_F("pending");
|
||||
case ACP_STATE_ARMING:
|
||||
return ESPHOME_F("arming");
|
||||
case ACP_STATE_DISARMING:
|
||||
return ESPHOME_F("disarming");
|
||||
case ACP_STATE_TRIGGERED:
|
||||
return ESPHOME_F("triggered");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
|
||||
: alarm_control_panel_(alarm_control_panel) {}
|
||||
void MQTTAlarmControlPanelComponent::setup() {
|
||||
@@ -112,9 +85,43 @@ const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return th
|
||||
|
||||
bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); }
|
||||
bool MQTTAlarmControlPanelComponent::publish_state() {
|
||||
const char *state_s;
|
||||
switch (this->alarm_control_panel_->get_state()) {
|
||||
case ACP_STATE_DISARMED:
|
||||
state_s = "disarmed";
|
||||
break;
|
||||
case ACP_STATE_ARMED_HOME:
|
||||
state_s = "armed_home";
|
||||
break;
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
state_s = "armed_away";
|
||||
break;
|
||||
case ACP_STATE_ARMED_NIGHT:
|
||||
state_s = "armed_night";
|
||||
break;
|
||||
case ACP_STATE_ARMED_VACATION:
|
||||
state_s = "armed_vacation";
|
||||
break;
|
||||
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||
state_s = "armed_custom_bypass";
|
||||
break;
|
||||
case ACP_STATE_PENDING:
|
||||
state_s = "pending";
|
||||
break;
|
||||
case ACP_STATE_ARMING:
|
||||
state_s = "arming";
|
||||
break;
|
||||
case ACP_STATE_DISARMING:
|
||||
state_s = "disarming";
|
||||
break;
|
||||
case ACP_STATE_TRIGGERED:
|
||||
state_s = "triggered";
|
||||
break;
|
||||
default:
|
||||
state_s = "unknown";
|
||||
}
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish(this->get_state_topic_to_(topic_buf),
|
||||
alarm_state_to_mqtt_str(this->alarm_control_panel_->get_state()));
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -564,8 +564,8 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t
|
||||
}
|
||||
|
||||
bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) {
|
||||
auto message = json::build_json(f);
|
||||
return this->publish(topic, message.c_str(), message.size(), qos, retain);
|
||||
std::string message = json::build_json(f);
|
||||
return this->publish(topic, message.c_str(), message.length(), qos, retain);
|
||||
}
|
||||
|
||||
void MQTTClientComponent::enable() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "mqtt_climate.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -13,111 +12,6 @@ static const char *const TAG = "mqtt.climate";
|
||||
|
||||
using namespace esphome::climate;
|
||||
|
||||
static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) {
|
||||
switch (mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_MODE_HEAT_COOL:
|
||||
return ESPHOME_F("heat_cool");
|
||||
case CLIMATE_MODE_AUTO:
|
||||
return ESPHOME_F("auto");
|
||||
case CLIMATE_MODE_COOL:
|
||||
return ESPHOME_F("cool");
|
||||
case CLIMATE_MODE_HEAT:
|
||||
return ESPHOME_F("heat");
|
||||
case CLIMATE_MODE_FAN_ONLY:
|
||||
return ESPHOME_F("fan_only");
|
||||
case CLIMATE_MODE_DRY:
|
||||
return ESPHOME_F("dry");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) {
|
||||
switch (action) {
|
||||
case CLIMATE_ACTION_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_ACTION_COOLING:
|
||||
return ESPHOME_F("cooling");
|
||||
case CLIMATE_ACTION_HEATING:
|
||||
return ESPHOME_F("heating");
|
||||
case CLIMATE_ACTION_IDLE:
|
||||
return ESPHOME_F("idle");
|
||||
case CLIMATE_ACTION_DRYING:
|
||||
return ESPHOME_F("drying");
|
||||
case CLIMATE_ACTION_FAN:
|
||||
return ESPHOME_F("fan");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) {
|
||||
switch (fan_mode) {
|
||||
case CLIMATE_FAN_ON:
|
||||
return ESPHOME_F("on");
|
||||
case CLIMATE_FAN_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_FAN_AUTO:
|
||||
return ESPHOME_F("auto");
|
||||
case CLIMATE_FAN_LOW:
|
||||
return ESPHOME_F("low");
|
||||
case CLIMATE_FAN_MEDIUM:
|
||||
return ESPHOME_F("medium");
|
||||
case CLIMATE_FAN_HIGH:
|
||||
return ESPHOME_F("high");
|
||||
case CLIMATE_FAN_MIDDLE:
|
||||
return ESPHOME_F("middle");
|
||||
case CLIMATE_FAN_FOCUS:
|
||||
return ESPHOME_F("focus");
|
||||
case CLIMATE_FAN_DIFFUSE:
|
||||
return ESPHOME_F("diffuse");
|
||||
case CLIMATE_FAN_QUIET:
|
||||
return ESPHOME_F("quiet");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) {
|
||||
switch (swing_mode) {
|
||||
case CLIMATE_SWING_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_SWING_BOTH:
|
||||
return ESPHOME_F("both");
|
||||
case CLIMATE_SWING_VERTICAL:
|
||||
return ESPHOME_F("vertical");
|
||||
case CLIMATE_SWING_HORIZONTAL:
|
||||
return ESPHOME_F("horizontal");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) {
|
||||
switch (preset) {
|
||||
case CLIMATE_PRESET_NONE:
|
||||
return ESPHOME_F("none");
|
||||
case CLIMATE_PRESET_HOME:
|
||||
return ESPHOME_F("home");
|
||||
case CLIMATE_PRESET_ECO:
|
||||
return ESPHOME_F("eco");
|
||||
case CLIMATE_PRESET_AWAY:
|
||||
return ESPHOME_F("away");
|
||||
case CLIMATE_PRESET_BOOST:
|
||||
return ESPHOME_F("boost");
|
||||
case CLIMATE_PRESET_COMFORT:
|
||||
return ESPHOME_F("comfort");
|
||||
case CLIMATE_PRESET_SLEEP:
|
||||
return ESPHOME_F("sleep");
|
||||
case CLIMATE_PRESET_ACTIVITY:
|
||||
return ESPHOME_F("activity");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
auto traits = this->device_->get_traits();
|
||||
@@ -366,8 +260,34 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device
|
||||
bool MQTTClimateComponent::publish_state_() {
|
||||
auto traits = this->device_->get_traits();
|
||||
// mode
|
||||
const char *mode_s;
|
||||
switch (this->device_->mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
mode_s = "off";
|
||||
break;
|
||||
case CLIMATE_MODE_AUTO:
|
||||
mode_s = "auto";
|
||||
break;
|
||||
case CLIMATE_MODE_COOL:
|
||||
mode_s = "cool";
|
||||
break;
|
||||
case CLIMATE_MODE_HEAT:
|
||||
mode_s = "heat";
|
||||
break;
|
||||
case CLIMATE_MODE_FAN_ONLY:
|
||||
mode_s = "fan_only";
|
||||
break;
|
||||
case CLIMATE_MODE_DRY:
|
||||
mode_s = "dry";
|
||||
break;
|
||||
case CLIMATE_MODE_HEAT_COOL:
|
||||
mode_s = "heat_cool";
|
||||
break;
|
||||
default:
|
||||
mode_s = "unknown";
|
||||
}
|
||||
bool success = true;
|
||||
if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode)))
|
||||
if (!this->publish(this->get_mode_state_topic(), mode_s))
|
||||
success = false;
|
||||
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
|
||||
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
|
||||
@@ -407,37 +327,134 @@ bool MQTTClimateComponent::publish_state_() {
|
||||
}
|
||||
|
||||
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
|
||||
if (this->device_->has_custom_preset()) {
|
||||
if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset()))
|
||||
success = false;
|
||||
} else if (this->device_->preset.has_value()) {
|
||||
if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value())))
|
||||
success = false;
|
||||
} else if (!this->publish(this->get_preset_state_topic(), "")) {
|
||||
success = false;
|
||||
std::string payload;
|
||||
if (this->device_->preset.has_value()) {
|
||||
switch (this->device_->preset.value()) {
|
||||
case CLIMATE_PRESET_NONE:
|
||||
payload = "none";
|
||||
break;
|
||||
case CLIMATE_PRESET_HOME:
|
||||
payload = "home";
|
||||
break;
|
||||
case CLIMATE_PRESET_AWAY:
|
||||
payload = "away";
|
||||
break;
|
||||
case CLIMATE_PRESET_BOOST:
|
||||
payload = "boost";
|
||||
break;
|
||||
case CLIMATE_PRESET_COMFORT:
|
||||
payload = "comfort";
|
||||
break;
|
||||
case CLIMATE_PRESET_ECO:
|
||||
payload = "eco";
|
||||
break;
|
||||
case CLIMATE_PRESET_SLEEP:
|
||||
payload = "sleep";
|
||||
break;
|
||||
case CLIMATE_PRESET_ACTIVITY:
|
||||
payload = "activity";
|
||||
break;
|
||||
default:
|
||||
payload = "unknown";
|
||||
}
|
||||
}
|
||||
if (this->device_->has_custom_preset())
|
||||
payload = this->device_->get_custom_preset().c_str();
|
||||
if (!this->publish(this->get_preset_state_topic(), payload))
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||
if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action)))
|
||||
const char *payload;
|
||||
switch (this->device_->action) {
|
||||
case CLIMATE_ACTION_OFF:
|
||||
payload = "off";
|
||||
break;
|
||||
case CLIMATE_ACTION_COOLING:
|
||||
payload = "cooling";
|
||||
break;
|
||||
case CLIMATE_ACTION_HEATING:
|
||||
payload = "heating";
|
||||
break;
|
||||
case CLIMATE_ACTION_IDLE:
|
||||
payload = "idle";
|
||||
break;
|
||||
case CLIMATE_ACTION_DRYING:
|
||||
payload = "drying";
|
||||
break;
|
||||
case CLIMATE_ACTION_FAN:
|
||||
payload = "fan";
|
||||
break;
|
||||
default:
|
||||
payload = "unknown";
|
||||
}
|
||||
if (!this->publish(this->get_action_state_topic(), payload))
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (traits.get_supports_fan_modes()) {
|
||||
if (this->device_->has_custom_fan_mode()) {
|
||||
if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode()))
|
||||
success = false;
|
||||
} else if (this->device_->fan_mode.has_value()) {
|
||||
if (!this->publish(this->get_fan_mode_state_topic(),
|
||||
climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value())))
|
||||
success = false;
|
||||
} else if (!this->publish(this->get_fan_mode_state_topic(), "")) {
|
||||
success = false;
|
||||
std::string payload;
|
||||
if (this->device_->fan_mode.has_value()) {
|
||||
switch (this->device_->fan_mode.value()) {
|
||||
case CLIMATE_FAN_ON:
|
||||
payload = "on";
|
||||
break;
|
||||
case CLIMATE_FAN_OFF:
|
||||
payload = "off";
|
||||
break;
|
||||
case CLIMATE_FAN_AUTO:
|
||||
payload = "auto";
|
||||
break;
|
||||
case CLIMATE_FAN_LOW:
|
||||
payload = "low";
|
||||
break;
|
||||
case CLIMATE_FAN_MEDIUM:
|
||||
payload = "medium";
|
||||
break;
|
||||
case CLIMATE_FAN_HIGH:
|
||||
payload = "high";
|
||||
break;
|
||||
case CLIMATE_FAN_MIDDLE:
|
||||
payload = "middle";
|
||||
break;
|
||||
case CLIMATE_FAN_FOCUS:
|
||||
payload = "focus";
|
||||
break;
|
||||
case CLIMATE_FAN_DIFFUSE:
|
||||
payload = "diffuse";
|
||||
break;
|
||||
case CLIMATE_FAN_QUIET:
|
||||
payload = "quiet";
|
||||
break;
|
||||
default:
|
||||
payload = "unknown";
|
||||
}
|
||||
}
|
||||
if (this->device_->has_custom_fan_mode())
|
||||
payload = this->device_->get_custom_fan_mode().c_str();
|
||||
if (!this->publish(this->get_fan_mode_state_topic(), payload))
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
|
||||
const char *payload;
|
||||
switch (this->device_->swing_mode) {
|
||||
case CLIMATE_SWING_OFF:
|
||||
payload = "off";
|
||||
break;
|
||||
case CLIMATE_SWING_BOTH:
|
||||
payload = "both";
|
||||
break;
|
||||
case CLIMATE_SWING_VERTICAL:
|
||||
payload = "vertical";
|
||||
break;
|
||||
case CLIMATE_SWING_HORIZONTAL:
|
||||
payload = "horizontal";
|
||||
break;
|
||||
default:
|
||||
payload = "unknown";
|
||||
}
|
||||
if (!this->publish(this->get_swing_mode_state_topic(), payload))
|
||||
success = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
@@ -150,22 +149,6 @@ bool MQTTComponent::publish(const char *topic, const char *payload) {
|
||||
return this->publish(topic, payload, strlen(payload));
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
bool MQTTComponent::publish(const std::string &topic, ProgmemStr payload) {
|
||||
return this->publish(topic.c_str(), payload);
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const char *topic, ProgmemStr payload) {
|
||||
if (topic[0] == '\0')
|
||||
return false;
|
||||
// On ESP8266, ProgmemStr is __FlashStringHelper* - need to copy from flash
|
||||
char buf[64];
|
||||
strncpy_P(buf, reinterpret_cast<const char *>(payload), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
return global_mqtt_client->publish(topic, buf, strlen(buf), this->qos_, this->retain_);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
|
||||
return this->publish_json(topic.c_str(), f);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "mqtt_client.h"
|
||||
|
||||
@@ -158,15 +157,6 @@ class MQTTComponent : public Component {
|
||||
*/
|
||||
bool publish(const std::string &topic, const char *payload, size_t payload_length);
|
||||
|
||||
/** Send a MQTT message.
|
||||
*
|
||||
* @param topic The topic.
|
||||
* @param payload The null-terminated payload.
|
||||
*/
|
||||
bool publish(const std::string &topic, const char *payload) {
|
||||
return this->publish(topic.c_str(), payload, strlen(payload));
|
||||
}
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as C string.
|
||||
@@ -199,29 +189,6 @@ class MQTTComponent : public Component {
|
||||
*/
|
||||
bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); }
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
/** Send a MQTT message with a PROGMEM string payload.
|
||||
*
|
||||
* @param topic The topic.
|
||||
* @param payload The payload (ProgmemStr - stored in flash on ESP8266).
|
||||
*/
|
||||
bool publish(const std::string &topic, ProgmemStr payload);
|
||||
|
||||
/** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as C string.
|
||||
* @param payload The payload (ProgmemStr - stored in flash on ESP8266).
|
||||
*/
|
||||
bool publish(const char *topic, ProgmemStr payload);
|
||||
|
||||
/** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
|
||||
* @param payload The payload (ProgmemStr - stored in flash on ESP8266).
|
||||
*/
|
||||
bool publish(StringRef topic, ProgmemStr payload) { return this->publish(topic.c_str(), payload); }
|
||||
#endif
|
||||
|
||||
/** Construct and send a JSON MQTT message.
|
||||
*
|
||||
* @param topic The topic.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "mqtt_cover.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -13,20 +12,6 @@ static const char *const TAG = "mqtt.cover";
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
static ProgmemStr cover_state_to_mqtt_str(CoverOperation operation, float position, bool supports_position) {
|
||||
if (operation == COVER_OPERATION_OPENING)
|
||||
return ESPHOME_F("opening");
|
||||
if (operation == COVER_OPERATION_CLOSING)
|
||||
return ESPHOME_F("closing");
|
||||
if (position == COVER_CLOSED)
|
||||
return ESPHOME_F("closed");
|
||||
if (position == COVER_OPEN)
|
||||
return ESPHOME_F("open");
|
||||
if (supports_position)
|
||||
return ESPHOME_F("open");
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
|
||||
MQTTCoverComponent::MQTTCoverComponent(Cover *cover) : cover_(cover) {}
|
||||
void MQTTCoverComponent::setup() {
|
||||
auto traits = this->cover_->get_traits();
|
||||
@@ -124,10 +109,14 @@ bool MQTTCoverComponent::publish_state() {
|
||||
if (!this->publish(this->get_tilt_state_topic(), pos, len))
|
||||
success = false;
|
||||
}
|
||||
const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening"
|
||||
: this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing"
|
||||
: this->cover_->position == COVER_CLOSED ? "closed"
|
||||
: this->cover_->position == COVER_OPEN ? "open"
|
||||
: traits.get_supports_position() ? "open"
|
||||
: "unknown";
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf),
|
||||
cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
|
||||
traits.get_supports_position())))
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
|
||||
success = false;
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "mqtt_fan.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -13,14 +12,6 @@ static const char *const TAG = "mqtt.fan";
|
||||
|
||||
using namespace esphome::fan;
|
||||
|
||||
static ProgmemStr fan_direction_to_mqtt_str(FanDirection direction) {
|
||||
return direction == FanDirection::FORWARD ? ESPHOME_F("forward") : ESPHOME_F("reverse");
|
||||
}
|
||||
|
||||
static ProgmemStr fan_oscillation_to_mqtt_str(bool oscillating) {
|
||||
return oscillating ? ESPHOME_F("oscillate_on") : ESPHOME_F("oscillate_off");
|
||||
}
|
||||
|
||||
MQTTFanComponent::MQTTFanComponent(Fan *state) : state_(state) {}
|
||||
|
||||
Fan *MQTTFanComponent::get_state() const { return this->state_; }
|
||||
@@ -173,12 +164,13 @@ bool MQTTFanComponent::publish_state() {
|
||||
this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
bool failed = false;
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction));
|
||||
bool success = this->publish(this->get_direction_state_topic(),
|
||||
this->state_->direction == fan::FanDirection::FORWARD ? "forward" : "reverse");
|
||||
failed = failed || !success;
|
||||
}
|
||||
if (this->state_->get_traits().supports_oscillation()) {
|
||||
bool success =
|
||||
this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating));
|
||||
bool success = this->publish(this->get_oscillation_state_topic(),
|
||||
this->state_->oscillating ? "oscillate_on" : "oscillate_off");
|
||||
failed = failed || !success;
|
||||
}
|
||||
auto traits = this->state_->get_traits();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "mqtt_valve.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -13,20 +12,6 @@ static const char *const TAG = "mqtt.valve";
|
||||
|
||||
using namespace esphome::valve;
|
||||
|
||||
static ProgmemStr valve_state_to_mqtt_str(ValveOperation operation, float position, bool supports_position) {
|
||||
if (operation == VALVE_OPERATION_OPENING)
|
||||
return ESPHOME_F("opening");
|
||||
if (operation == VALVE_OPERATION_CLOSING)
|
||||
return ESPHOME_F("closing");
|
||||
if (position == VALVE_CLOSED)
|
||||
return ESPHOME_F("closed");
|
||||
if (position == VALVE_OPEN)
|
||||
return ESPHOME_F("open");
|
||||
if (supports_position)
|
||||
return ESPHOME_F("open");
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
|
||||
MQTTValveComponent::MQTTValveComponent(Valve *valve) : valve_(valve) {}
|
||||
void MQTTValveComponent::setup() {
|
||||
auto traits = this->valve_->get_traits();
|
||||
@@ -93,10 +78,14 @@ bool MQTTValveComponent::publish_state() {
|
||||
if (!this->publish(this->get_position_state_topic(), pos, len))
|
||||
success = false;
|
||||
}
|
||||
const char *state_s = this->valve_->current_operation == VALVE_OPERATION_OPENING ? "opening"
|
||||
: this->valve_->current_operation == VALVE_OPERATION_CLOSING ? "closing"
|
||||
: this->valve_->position == VALVE_CLOSED ? "closed"
|
||||
: this->valve_->position == VALVE_OPEN ? "open"
|
||||
: traits.get_supports_position() ? "open"
|
||||
: "unknown";
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf),
|
||||
valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position,
|
||||
traits.get_supports_position())))
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
|
||||
success = false;
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,8 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
@coroutine_with_priority(CoroPriority.NETWORK)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_NETWORK")
|
||||
# ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed
|
||||
if CORE.using_arduino and CORE.is_esp32:
|
||||
cg.add_library("Networking", None)
|
||||
|
||||
# Apply high performance networking settings
|
||||
# Config can explicitly enable/disable, or default to component-driven behavior
|
||||
|
||||
@@ -4,7 +4,6 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
add_idf_sdkconfig_option,
|
||||
include_builtin_idf_component,
|
||||
only_on_variant,
|
||||
require_vfs_select,
|
||||
)
|
||||
@@ -173,9 +172,6 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Re-enable openthread IDF component (excluded by default)
|
||||
include_builtin_idf_component("openthread")
|
||||
|
||||
cg.add_define("USE_OPENTHREAD")
|
||||
|
||||
# OpenThread SRP needs access to mDNS services after setup
|
||||
|
||||
@@ -78,21 +78,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.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::sensor {
|
||||
|
||||
@@ -31,13 +30,20 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o
|
||||
}
|
||||
}
|
||||
|
||||
// State class strings indexed by StateClass enum (0-4): NONE, MEASUREMENT, TOTAL_INCREASING, TOTAL, MEASUREMENT_ANGLE
|
||||
PROGMEM_STRING_TABLE(StateClassStrings, "", "measurement", "total_increasing", "total", "measurement_angle");
|
||||
static_assert(StateClassStrings::COUNT == STATE_CLASS_LAST + 1, "StateClassStrings must match StateClass enum");
|
||||
|
||||
const LogString *state_class_to_string(StateClass state_class) {
|
||||
// Fallback to index 0 (empty string for STATE_CLASS_NONE) if out of range
|
||||
return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0);
|
||||
switch (state_class) {
|
||||
case STATE_CLASS_MEASUREMENT:
|
||||
return LOG_STR("measurement");
|
||||
case STATE_CLASS_TOTAL_INCREASING:
|
||||
return LOG_STR("total_increasing");
|
||||
case STATE_CLASS_TOTAL:
|
||||
return LOG_STR("total");
|
||||
case STATE_CLASS_MEASUREMENT_ANGLE:
|
||||
return LOG_STR("measurement_angle");
|
||||
case STATE_CLASS_NONE:
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
}
|
||||
|
||||
Sensor::Sensor() : state(NAN), raw_state(NAN) {}
|
||||
|
||||
@@ -32,7 +32,6 @@ enum StateClass : uint8_t {
|
||||
STATE_CLASS_TOTAL = 3,
|
||||
STATE_CLASS_MEASUREMENT_ANGLE = 4
|
||||
};
|
||||
constexpr uint8_t STATE_CLASS_LAST = static_cast<uint8_t>(STATE_CLASS_MEASUREMENT_ANGLE);
|
||||
|
||||
const LogString *state_class_to_string(StateClass state_class);
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
|
||||
#include "posix_tz.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
// Global timezone - set once at startup, rarely changes
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state
|
||||
static ParsedTimezone global_tz_{};
|
||||
|
||||
void set_global_tz(const ParsedTimezone &tz) { global_tz_ = tz; }
|
||||
|
||||
const ParsedTimezone &get_global_tz() { return global_tz_; }
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Helper to parse an unsigned integer from string, updating pointer
|
||||
static uint32_t parse_uint(const char *&p) {
|
||||
uint32_t value = 0;
|
||||
while (std::isdigit(static_cast<unsigned char>(*p))) {
|
||||
value = value * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); }
|
||||
|
||||
// Get days in year (avoids duplicate is_leap_year calls)
|
||||
static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; }
|
||||
|
||||
// Convert days since epoch to year, updating days to remainder
|
||||
static int __attribute__((noinline)) days_to_year(int64_t &days) {
|
||||
int year = 1970;
|
||||
int diy;
|
||||
while (days >= (diy = days_in_year(year))) {
|
||||
days -= diy;
|
||||
year++;
|
||||
}
|
||||
while (days < 0) {
|
||||
year--;
|
||||
days += days_in_year(year);
|
||||
}
|
||||
return year;
|
||||
}
|
||||
|
||||
// Extract just the year from a UTC epoch
|
||||
static int epoch_to_year(time_t epoch) {
|
||||
int64_t days = epoch / 86400;
|
||||
if (epoch < 0 && epoch % 86400 != 0)
|
||||
days--;
|
||||
return days_to_year(days);
|
||||
}
|
||||
|
||||
int days_in_month(int year, int month) {
|
||||
switch (month) {
|
||||
case 2:
|
||||
return is_leap_year(year) ? 29 : 28;
|
||||
case 4:
|
||||
case 6:
|
||||
case 9:
|
||||
case 11:
|
||||
return 30;
|
||||
default:
|
||||
return 31;
|
||||
}
|
||||
}
|
||||
|
||||
// Zeller-like algorithm for day of week (0 = Sunday)
|
||||
int __attribute__((noinline)) day_of_week(int year, int month, int day) {
|
||||
// Adjust for January/February
|
||||
if (month < 3) {
|
||||
month += 12;
|
||||
year--;
|
||||
}
|
||||
int k = year % 100;
|
||||
int j = year / 100;
|
||||
int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
|
||||
// Convert from Zeller (0=Sat) to standard (0=Sun)
|
||||
return ((h + 6) % 7);
|
||||
}
|
||||
|
||||
void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) {
|
||||
// Days since epoch
|
||||
int64_t days = epoch / 86400;
|
||||
int32_t remaining_secs = epoch % 86400;
|
||||
if (remaining_secs < 0) {
|
||||
days--;
|
||||
remaining_secs += 86400;
|
||||
}
|
||||
|
||||
out_tm->tm_sec = remaining_secs % 60;
|
||||
remaining_secs /= 60;
|
||||
out_tm->tm_min = remaining_secs % 60;
|
||||
out_tm->tm_hour = remaining_secs / 60;
|
||||
|
||||
// Day of week (Jan 1, 1970 was Thursday = 4)
|
||||
out_tm->tm_wday = static_cast<int>((days + 4) % 7);
|
||||
if (out_tm->tm_wday < 0)
|
||||
out_tm->tm_wday += 7;
|
||||
|
||||
// Calculate year (updates days to day-of-year)
|
||||
int year = days_to_year(days);
|
||||
out_tm->tm_year = year - 1900;
|
||||
out_tm->tm_yday = static_cast<int>(days);
|
||||
|
||||
// Calculate month and day
|
||||
int month = 1;
|
||||
int dim;
|
||||
while (days >= (dim = days_in_month(year, month))) {
|
||||
days -= dim;
|
||||
month++;
|
||||
}
|
||||
|
||||
out_tm->tm_mon = month - 1;
|
||||
out_tm->tm_mday = static_cast<int>(days) + 1;
|
||||
out_tm->tm_isdst = 0;
|
||||
}
|
||||
|
||||
bool skip_tz_name(const char *&p) {
|
||||
if (*p == '<') {
|
||||
// Angle-bracket quoted name: <+07>, <-03>, <AEST>
|
||||
p++; // skip '<'
|
||||
while (*p && *p != '>') {
|
||||
p++;
|
||||
}
|
||||
if (*p == '>') {
|
||||
p++; // skip '>'
|
||||
return true;
|
||||
}
|
||||
return false; // Unterminated
|
||||
}
|
||||
|
||||
// Standard name: 3+ letters
|
||||
const char *start = p;
|
||||
while (*p && std::isalpha(static_cast<unsigned char>(*p))) {
|
||||
p++;
|
||||
}
|
||||
return (p - start) >= 3;
|
||||
}
|
||||
|
||||
int32_t __attribute__((noinline)) parse_offset(const char *&p) {
|
||||
int sign = 1;
|
||||
if (*p == '-') {
|
||||
sign = -1;
|
||||
p++;
|
||||
} else if (*p == '+') {
|
||||
p++;
|
||||
}
|
||||
|
||||
int hours = parse_uint(p);
|
||||
int minutes = 0;
|
||||
int seconds = 0;
|
||||
|
||||
if (*p == ':') {
|
||||
p++;
|
||||
minutes = parse_uint(p);
|
||||
if (*p == ':') {
|
||||
p++;
|
||||
seconds = parse_uint(p);
|
||||
}
|
||||
}
|
||||
|
||||
return sign * (hours * 3600 + minutes * 60 + seconds);
|
||||
}
|
||||
|
||||
// Helper to parse the optional /time suffix (reuses parse_offset logic)
|
||||
static void parse_transition_time(const char *&p, DSTRule &rule) {
|
||||
rule.time_seconds = 2 * 3600; // Default 02:00
|
||||
if (*p == '/') {
|
||||
p++;
|
||||
rule.time_seconds = parse_offset(p);
|
||||
}
|
||||
}
|
||||
|
||||
void __attribute__((noinline)) julian_to_month_day(int julian_day, int &out_month, int &out_day) {
|
||||
// J format: day 1-365, Feb 29 is NOT counted even in leap years
|
||||
// So day 60 is always March 1
|
||||
// Iterate forward through months (no array needed)
|
||||
int remaining = julian_day;
|
||||
out_month = 1;
|
||||
while (out_month <= 12) {
|
||||
// Days in month for non-leap year (J format ignores leap years)
|
||||
int dim = days_in_month(2001, out_month); // 2001 is non-leap year
|
||||
if (remaining <= dim) {
|
||||
out_day = remaining;
|
||||
return;
|
||||
}
|
||||
remaining -= dim;
|
||||
out_month++;
|
||||
}
|
||||
out_day = remaining;
|
||||
}
|
||||
|
||||
void __attribute__((noinline)) day_of_year_to_month_day(int day_of_year, int year, int &out_month, int &out_day) {
|
||||
// Plain format: day 0-365, Feb 29 IS counted in leap years
|
||||
// Day 0 = Jan 1
|
||||
int remaining = day_of_year;
|
||||
out_month = 1;
|
||||
|
||||
while (out_month <= 12) {
|
||||
int days_this_month = days_in_month(year, out_month);
|
||||
if (remaining < days_this_month) {
|
||||
out_day = remaining + 1;
|
||||
return;
|
||||
}
|
||||
remaining -= days_this_month;
|
||||
out_month++;
|
||||
}
|
||||
|
||||
// Shouldn't reach here with valid input
|
||||
out_month = 12;
|
||||
out_day = 31;
|
||||
}
|
||||
|
||||
bool parse_dst_rule(const char *&p, DSTRule &rule) {
|
||||
rule = {}; // Zero initialize
|
||||
|
||||
if (*p == 'M' || *p == 'm') {
|
||||
// M format: Mm.w.d (month.week.day)
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
p++;
|
||||
|
||||
rule.month = parse_uint(p);
|
||||
if (rule.month < 1 || rule.month > 12)
|
||||
return false;
|
||||
|
||||
if (*p++ != '.')
|
||||
return false;
|
||||
|
||||
rule.week = parse_uint(p);
|
||||
if (rule.week < 1 || rule.week > 5)
|
||||
return false;
|
||||
|
||||
if (*p++ != '.')
|
||||
return false;
|
||||
|
||||
rule.day_of_week = parse_uint(p);
|
||||
if (rule.day_of_week > 6)
|
||||
return false;
|
||||
|
||||
} else if (*p == 'J' || *p == 'j') {
|
||||
// J format: Jn (Julian day 1-365, not counting Feb 29)
|
||||
rule.type = DSTRuleType::JULIAN_NO_LEAP;
|
||||
p++;
|
||||
|
||||
rule.day = parse_uint(p);
|
||||
if (rule.day < 1 || rule.day > 365)
|
||||
return false;
|
||||
|
||||
} else if (std::isdigit(static_cast<unsigned char>(*p))) {
|
||||
// Plain number format: n (day 0-365, counting Feb 29)
|
||||
rule.type = DSTRuleType::DAY_OF_YEAR;
|
||||
|
||||
rule.day = parse_uint(p);
|
||||
if (rule.day > 365)
|
||||
return false;
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse optional /time suffix
|
||||
parse_transition_time(p, rule);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate days from Jan 1 of given year to given month/day
|
||||
static int __attribute__((noinline)) days_from_year_start(int year, int month, int day) {
|
||||
int days = day - 1;
|
||||
for (int m = 1; m < month; m++) {
|
||||
days += days_in_month(year, m);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
// Calculate days from epoch to Jan 1 of given year (for DST transition calculations)
|
||||
// Only supports years >= 1970. Timezone is either compiled in from YAML or set by
|
||||
// Home Assistant, so pre-1970 dates are not a concern.
|
||||
static int64_t __attribute__((noinline)) days_to_year_start(int year) {
|
||||
int64_t days = 0;
|
||||
for (int y = 1970; y < year; y++) {
|
||||
days += days_in_year(y);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) {
|
||||
int month, day;
|
||||
|
||||
switch (rule.type) {
|
||||
case DSTRuleType::MONTH_WEEK_DAY: {
|
||||
// Find the nth occurrence of day_of_week in the given month
|
||||
int first_dow = day_of_week(year, rule.month, 1);
|
||||
|
||||
// Days until first occurrence of target day
|
||||
int days_until_first = (rule.day_of_week - first_dow + 7) % 7;
|
||||
int first_occurrence = 1 + days_until_first;
|
||||
|
||||
if (rule.week == 5) {
|
||||
// "Last" occurrence - find the last one in the month
|
||||
int dim = days_in_month(year, rule.month);
|
||||
day = first_occurrence;
|
||||
while (day + 7 <= dim) {
|
||||
day += 7;
|
||||
}
|
||||
} else {
|
||||
// nth occurrence
|
||||
day = first_occurrence + (rule.week - 1) * 7;
|
||||
}
|
||||
month = rule.month;
|
||||
break;
|
||||
}
|
||||
|
||||
case DSTRuleType::JULIAN_NO_LEAP:
|
||||
// J format: day 1-365, Feb 29 not counted
|
||||
julian_to_month_day(rule.day, month, day);
|
||||
break;
|
||||
|
||||
case DSTRuleType::DAY_OF_YEAR:
|
||||
// Plain format: day 0-365, Feb 29 counted
|
||||
day_of_year_to_month_day(rule.day, year, month, day);
|
||||
break;
|
||||
|
||||
case DSTRuleType::NONE:
|
||||
// Should never be called with NONE, but handle it gracefully
|
||||
month = 1;
|
||||
day = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate days from epoch to this date
|
||||
int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day);
|
||||
|
||||
// Convert to epoch and add transition time and base offset
|
||||
return days * 86400 + rule.time_seconds + base_offset_seconds;
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
bool __attribute__((noinline)) is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) {
|
||||
if (!tz.has_dst()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int year = internal::epoch_to_year(utc_epoch);
|
||||
|
||||
// Calculate DST start and end for this year
|
||||
// DST start transition happens in standard time
|
||||
time_t dst_start = internal::calculate_dst_transition(year, tz.dst_start, tz.std_offset_seconds);
|
||||
// DST end transition happens in daylight time
|
||||
time_t dst_end = internal::calculate_dst_transition(year, tz.dst_end, tz.dst_offset_seconds);
|
||||
|
||||
if (dst_start < dst_end) {
|
||||
// Northern hemisphere: DST is between start and end
|
||||
return (utc_epoch >= dst_start && utc_epoch < dst_end);
|
||||
} else {
|
||||
// Southern hemisphere: DST is outside the range (wraps around year)
|
||||
return (utc_epoch >= dst_start || utc_epoch < dst_end);
|
||||
}
|
||||
}
|
||||
|
||||
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) {
|
||||
if (!tz_string || !*tz_string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *p = tz_string;
|
||||
|
||||
// Initialize result (dst_start/dst_end default to type=NONE, so has_dst() returns false)
|
||||
result.std_offset_seconds = 0;
|
||||
result.dst_offset_seconds = 0;
|
||||
result.dst_start = {};
|
||||
result.dst_end = {};
|
||||
|
||||
// Skip standard timezone name
|
||||
if (!internal::skip_tz_name(p)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse standard offset (required)
|
||||
if (!*p || (!std::isdigit(static_cast<unsigned char>(*p)) && *p != '+' && *p != '-')) {
|
||||
return false;
|
||||
}
|
||||
result.std_offset_seconds = internal::parse_offset(p);
|
||||
|
||||
// Check for DST name
|
||||
if (!*p) {
|
||||
return true; // No DST
|
||||
}
|
||||
|
||||
// If next char is comma, there's no DST name but there are rules (invalid)
|
||||
if (*p == ',') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there's something that looks like a DST name start
|
||||
// (letter or angle bracket). If not, treat as trailing garbage and return success.
|
||||
if (!std::isalpha(static_cast<unsigned char>(*p)) && *p != '<') {
|
||||
return true; // No DST, trailing characters ignored
|
||||
}
|
||||
|
||||
if (!internal::skip_tz_name(p)) {
|
||||
return false; // Invalid DST name (started but malformed)
|
||||
}
|
||||
|
||||
// Optional DST offset (default is std - 1 hour)
|
||||
if (*p && *p != ',' && (std::isdigit(static_cast<unsigned char>(*p)) || *p == '+' || *p == '-')) {
|
||||
result.dst_offset_seconds = internal::parse_offset(p);
|
||||
} else {
|
||||
result.dst_offset_seconds = result.std_offset_seconds - 3600;
|
||||
}
|
||||
|
||||
// Parse DST rules (required when DST name is present)
|
||||
if (*p != ',') {
|
||||
// DST name without rules - treat as no DST since we can't determine transitions
|
||||
return true;
|
||||
}
|
||||
|
||||
p++;
|
||||
if (!internal::parse_dst_rule(p, result.dst_start)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Second rule is required per POSIX
|
||||
if (*p != ',') {
|
||||
return false;
|
||||
}
|
||||
p++;
|
||||
// has_dst() now returns true since dst_start.type was set by parse_dst_rule
|
||||
return internal::parse_dst_rule(p, result.dst_end);
|
||||
}
|
||||
|
||||
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) {
|
||||
if (!out_tm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine DST status once (avoids duplicate is_in_dst calculation)
|
||||
bool in_dst = is_in_dst(utc_epoch, tz);
|
||||
int32_t offset = in_dst ? tz.dst_offset_seconds : tz.std_offset_seconds;
|
||||
|
||||
// Apply offset (POSIX offset is positive west, so subtract to get local)
|
||||
time_t local_epoch = utc_epoch - offset;
|
||||
|
||||
internal::epoch_to_tm_utc(local_epoch, out_tm);
|
||||
out_tm->tm_isdst = in_dst ? 1 : 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
#ifndef USE_HOST
|
||||
// Override libc's localtime functions to use our timezone on embedded platforms.
|
||||
// This allows user lambdas calling ::localtime() to get correct local time
|
||||
// without needing the TZ environment variable (which pulls in scanf bloat).
|
||||
// On host, we use the normal TZ mechanism since there's no memory constraint.
|
||||
|
||||
// Thread-safe version
|
||||
extern "C" struct tm *localtime_r(const time_t *timer, struct tm *result) {
|
||||
if (timer == nullptr || result == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
esphome::time::epoch_to_local_tm(*timer, esphome::time::get_global_tz(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Non-thread-safe version (uses static buffer, standard libc behavior)
|
||||
extern "C" struct tm *localtime(const time_t *timer) {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static struct tm localtime_buf;
|
||||
return localtime_r(timer, &localtime_buf);
|
||||
}
|
||||
#endif // !USE_HOST
|
||||
|
||||
#endif // USE_TIME_TIMEZONE
|
||||
@@ -1,132 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
/// Type of DST transition rule
|
||||
enum class DSTRuleType : uint8_t {
|
||||
NONE = 0, ///< No DST rule (used to indicate no DST)
|
||||
MONTH_WEEK_DAY, ///< M format: Mm.w.d (e.g., M3.2.0 = 2nd Sunday of March)
|
||||
JULIAN_NO_LEAP, ///< J format: Jn (day 1-365, Feb 29 not counted)
|
||||
DAY_OF_YEAR, ///< Plain number: n (day 0-365, Feb 29 counted in leap years)
|
||||
};
|
||||
|
||||
/// Rule for DST transition (packed for 32-bit: 12 bytes)
|
||||
struct DSTRule {
|
||||
int32_t time_seconds; ///< Seconds after midnight (default 7200 = 2:00 AM)
|
||||
uint16_t day; ///< Day of year (for JULIAN_NO_LEAP and DAY_OF_YEAR)
|
||||
DSTRuleType type; ///< Type of rule
|
||||
uint8_t month; ///< Month 1-12 (for MONTH_WEEK_DAY)
|
||||
uint8_t week; ///< Week 1-5, 5 = last (for MONTH_WEEK_DAY)
|
||||
uint8_t day_of_week; ///< Day 0-6, 0 = Sunday (for MONTH_WEEK_DAY)
|
||||
};
|
||||
|
||||
/// Parsed POSIX timezone information (packed for 32-bit: 32 bytes)
|
||||
struct ParsedTimezone {
|
||||
int32_t std_offset_seconds; ///< Standard time offset from UTC in seconds (positive = west)
|
||||
int32_t dst_offset_seconds; ///< DST offset from UTC in seconds
|
||||
DSTRule dst_start; ///< When DST starts
|
||||
DSTRule dst_end; ///< When DST ends
|
||||
|
||||
/// Check if this timezone has DST rules
|
||||
bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; }
|
||||
};
|
||||
|
||||
/// Parse a POSIX TZ string into a ParsedTimezone struct.
|
||||
/// Supports formats like:
|
||||
/// - "EST5" (simple offset, no DST)
|
||||
/// - "EST5EDT,M3.2.0,M11.1.0" (with DST, M-format rules)
|
||||
/// - "CST6CDT,M3.2.0/2,M11.1.0/2" (with transition times)
|
||||
/// - "<+07>-7" (angle-bracket notation for special names)
|
||||
/// - "IST-5:30" (half-hour offsets)
|
||||
/// - "EST5EDT,J60,J300" (J-format: Julian day without leap day)
|
||||
/// - "EST5EDT,60,300" (plain day number: day of year with leap day)
|
||||
/// @param tz_string The POSIX TZ string to parse
|
||||
/// @param result Output: the parsed timezone data
|
||||
/// @return true if parsing succeeded, false on error
|
||||
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result);
|
||||
|
||||
/// Convert a UTC epoch to local time using the parsed timezone.
|
||||
/// This replaces libc's localtime() to avoid scanf dependency.
|
||||
/// @param utc_epoch Unix timestamp in UTC
|
||||
/// @param tz The parsed timezone
|
||||
/// @param[out] out_tm Output tm struct with local time
|
||||
/// @return true on success
|
||||
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm);
|
||||
|
||||
/// Set the global timezone used by epoch_to_local_tm() when called without a timezone.
|
||||
/// This is called by RealTimeClock::apply_timezone_() to enable ESPTime::from_epoch_local()
|
||||
/// to work without libc's localtime().
|
||||
void set_global_tz(const ParsedTimezone &tz);
|
||||
|
||||
/// Get the global timezone.
|
||||
const ParsedTimezone &get_global_tz();
|
||||
|
||||
/// Check if a given UTC epoch falls within DST for the parsed timezone.
|
||||
/// @param utc_epoch Unix timestamp in UTC
|
||||
/// @param tz The parsed timezone
|
||||
/// @return true if DST is in effect at the given time
|
||||
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz);
|
||||
|
||||
// Internal helper functions exposed for testing
|
||||
|
||||
namespace internal {
|
||||
|
||||
/// Skip a timezone name (letters or <...> quoted format)
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @return true if a valid name was found
|
||||
bool skip_tz_name(const char *&p);
|
||||
|
||||
/// Parse an offset in format [-]hh[:mm[:ss]]
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @return Offset in seconds
|
||||
int32_t parse_offset(const char *&p);
|
||||
|
||||
/// Parse a DST rule in format Mm.w.d[/time], Jn[/time], or n[/time]
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @param rule Output: the parsed rule
|
||||
/// @return true if parsing succeeded
|
||||
bool parse_dst_rule(const char *&p, DSTRule &rule);
|
||||
|
||||
/// Convert Julian day (J format, 1-365 not counting Feb 29) to month/day
|
||||
/// @param julian_day Day number 1-365
|
||||
/// @param[out] month Output: month 1-12
|
||||
/// @param[out] day Output: day of month
|
||||
void julian_to_month_day(int julian_day, int &month, int &day);
|
||||
|
||||
/// Convert day of year (plain format, 0-365 counting Feb 29) to month/day
|
||||
/// @param day_of_year Day number 0-365
|
||||
/// @param year The year (for leap year calculation)
|
||||
/// @param[out] month Output: month 1-12
|
||||
/// @param[out] day Output: day of month
|
||||
void day_of_year_to_month_day(int day_of_year, int year, int &month, int &day);
|
||||
|
||||
/// Calculate day of week for any date (0 = Sunday)
|
||||
/// Uses a simplified algorithm that works for years 1970-2099
|
||||
int day_of_week(int year, int month, int day);
|
||||
|
||||
/// Get the number of days in a month
|
||||
int days_in_month(int year, int month);
|
||||
|
||||
/// Check if a year is a leap year
|
||||
bool is_leap_year(int year);
|
||||
|
||||
/// Convert epoch to year/month/day/hour/min/sec (UTC)
|
||||
void epoch_to_tm_utc(time_t epoch, struct tm *out_tm);
|
||||
|
||||
/// Calculate the epoch timestamp for a DST transition in a given year.
|
||||
/// @param year The year (e.g., 2026)
|
||||
/// @param rule The DST rule (month, week, day_of_week, time)
|
||||
/// @param base_offset_seconds The timezone offset to apply (std or dst depending on context)
|
||||
/// @return Unix epoch timestamp of the transition
|
||||
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
#endif // USE_TIME_TIMEZONE
|
||||
@@ -14,8 +14,8 @@
|
||||
#include <sys/time.h>
|
||||
#endif
|
||||
#include <cerrno>
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
@@ -23,33 +23,9 @@ static const char *const TAG = "time";
|
||||
|
||||
RealTimeClock::RealTimeClock() = default;
|
||||
|
||||
ESPTime __attribute__((noinline)) RealTimeClock::now() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
time_t epoch = this->timestamp_now();
|
||||
struct tm local_tm;
|
||||
if (epoch_to_local_tm(epoch, get_global_tz(), &local_tm)) {
|
||||
return ESPTime::from_c_tm(&local_tm, epoch);
|
||||
}
|
||||
// Fallback to UTC if parsing failed
|
||||
return ESPTime::from_epoch_utc(epoch);
|
||||
#else
|
||||
return ESPTime::from_epoch_local(this->timestamp_now());
|
||||
#endif
|
||||
}
|
||||
|
||||
void RealTimeClock::dump_config() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
const auto &tz = get_global_tz();
|
||||
// POSIX offset is positive west, negate for conventional UTC+X display
|
||||
int std_h = -tz.std_offset_seconds / 3600;
|
||||
int std_m = (std::abs(tz.std_offset_seconds) % 3600) / 60;
|
||||
if (tz.has_dst()) {
|
||||
int dst_h = -tz.dst_offset_seconds / 3600;
|
||||
int dst_m = (std::abs(tz.dst_offset_seconds) % 3600) / 60;
|
||||
ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d (DST UTC%+d:%02d)", std_h, std_m, dst_h, dst_m);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d", std_h, std_m);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
|
||||
#endif
|
||||
auto time = this->now();
|
||||
ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour,
|
||||
@@ -96,6 +72,11 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
ret = settimeofday(&timev, nullptr);
|
||||
}
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
// Move timezone back to local timezone.
|
||||
this->apply_timezone_();
|
||||
#endif
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
|
||||
}
|
||||
@@ -108,29 +89,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
}
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
void RealTimeClock::apply_timezone_(const char *tz) {
|
||||
ParsedTimezone parsed{};
|
||||
|
||||
// Handle null or empty input - use UTC
|
||||
if (tz == nullptr || *tz == '\0') {
|
||||
set_global_tz(parsed);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_HOST
|
||||
// On host platform, also set TZ environment variable for libc compatibility
|
||||
setenv("TZ", tz, 1);
|
||||
void RealTimeClock::apply_timezone_() {
|
||||
setenv("TZ", this->timezone_.c_str(), 1);
|
||||
tzset();
|
||||
#endif
|
||||
|
||||
// Parse the POSIX TZ string using our custom parser
|
||||
if (!parse_posix_tz(tz, parsed)) {
|
||||
ESP_LOGW(TAG, "Failed to parse timezone: %s", tz);
|
||||
// parsed stays as default (UTC) on failure
|
||||
}
|
||||
|
||||
// Set global timezone for all time conversions
|
||||
set_global_tz(parsed);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/time.h"
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
#include "posix_tz.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
@@ -23,31 +20,26 @@ class RealTimeClock : public PollingComponent {
|
||||
explicit RealTimeClock();
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
/// Set the time zone from a POSIX TZ string.
|
||||
void set_timezone(const char *tz) { this->apply_timezone_(tz); }
|
||||
|
||||
/// Set the time zone from a character buffer with known length.
|
||||
/// The buffer does not need to be null-terminated.
|
||||
void set_timezone(const char *tz, size_t len) {
|
||||
if (tz == nullptr) {
|
||||
this->apply_timezone_(nullptr);
|
||||
return;
|
||||
}
|
||||
// Stack buffer - TZ strings from tzdata are typically short (< 50 chars)
|
||||
char buf[128];
|
||||
if (len >= sizeof(buf))
|
||||
len = sizeof(buf) - 1;
|
||||
memcpy(buf, tz, len);
|
||||
buf[len] = '\0';
|
||||
this->apply_timezone_(buf);
|
||||
/// Set the time zone.
|
||||
void set_timezone(const std::string &tz) {
|
||||
this->timezone_ = tz;
|
||||
this->apply_timezone_();
|
||||
}
|
||||
|
||||
/// Set the time zone from a std::string.
|
||||
void set_timezone(const std::string &tz) { this->apply_timezone_(tz.c_str()); }
|
||||
/// Set the time zone from raw buffer, only if it differs from the current one.
|
||||
void set_timezone(const char *tz, size_t len) {
|
||||
if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) {
|
||||
this->timezone_.assign(tz, len);
|
||||
this->apply_timezone_();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the time zone currently in use.
|
||||
std::string get_timezone() { return this->timezone_; }
|
||||
#endif
|
||||
|
||||
/// Get the time in the currently defined timezone.
|
||||
ESPTime now();
|
||||
ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); }
|
||||
|
||||
/// Get the time without any time zone or DST corrections.
|
||||
ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
|
||||
@@ -66,10 +58,11 @@ class RealTimeClock : public PollingComponent {
|
||||
void synchronize_epoch_(uint32_t epoch);
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
void apply_timezone_(const char *tz);
|
||||
std::string timezone_{};
|
||||
void apply_timezone_();
|
||||
#endif
|
||||
|
||||
LazyCallbackManager<void()> time_sync_callback_;
|
||||
CallbackManager<void()> time_sync_callback_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class TimeHasTimeCondition : public Condition<Ts...> {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
@@ -23,11 +23,17 @@ const LogString *valve_command_to_str(float pos) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
// Valve operation strings indexed by ValveOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN
|
||||
PROGMEM_STRING_TABLE(ValveOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN");
|
||||
|
||||
const LogString *valve_operation_to_str(ValveOperation op) {
|
||||
return ValveOperationStrings::get_log_str(static_cast<uint8_t>(op), ValveOperationStrings::LAST_INDEX);
|
||||
switch (op) {
|
||||
case VALVE_OPERATION_IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case VALVE_OPERATION_OPENING:
|
||||
return LOG_STR("OPENING");
|
||||
case VALVE_OPERATION_CLOSING:
|
||||
return LOG_STR("CLOSING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
Valve::Valve() : position{VALVE_OPEN} {}
|
||||
|
||||
@@ -430,14 +430,12 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr
|
||||
}
|
||||
|
||||
if (this->api_client_ != nullptr) {
|
||||
char current_peername[socket::SOCKADDR_STR_LEN];
|
||||
char new_peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGE(TAG,
|
||||
"Multiple API Clients attempting to connect to Voice Assistant\n"
|
||||
"Current client: %s (%s)\n"
|
||||
"New client: %s (%s)",
|
||||
this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(),
|
||||
client->get_peername_to(new_peername));
|
||||
this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(),
|
||||
client->get_peername());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ struct Timer {
|
||||
}
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
std::string to_string() const { // NOLINT
|
||||
std::string to_string() const {
|
||||
char buffer[TO_STR_BUFFER_SIZE];
|
||||
return this->to_str(buffer);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Trigger CI memory impact (uses updated ESPAsyncWebServer from web_server_base)
|
||||
#include "web_server.h"
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/components/json/json_util.h"
|
||||
@@ -210,7 +209,7 @@ void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_
|
||||
void DeferredUpdateEventSource::process_deferred_queue_() {
|
||||
while (!deferred_queue_.empty()) {
|
||||
DeferredEvent &de = deferred_queue_.front();
|
||||
auto message = de.message_generator_(web_server_, de.source_);
|
||||
std::string message = de.message_generator_(web_server_, de.source_);
|
||||
if (this->send(message.c_str(), "state") != DISCARDED) {
|
||||
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
|
||||
deferred_queue_.erase(deferred_queue_.begin());
|
||||
@@ -267,7 +266,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
|
||||
// deferred queue still not empty which means downstream event queue full, no point trying to send first
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
} else {
|
||||
auto message = message_generator(web_server_, source);
|
||||
std::string message = message_generator(web_server_, source);
|
||||
if (this->send(message.c_str(), "state") == DISCARDED) {
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
} else {
|
||||
@@ -321,7 +320,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
|
||||
ws->defer([ws, source]() {
|
||||
// Configure reconnect timeout and send config
|
||||
// this should always go through since the AsyncEventSourceClient event queue is empty on connect
|
||||
auto message = ws->get_config_json();
|
||||
std::string message = ws->get_config_json();
|
||||
source->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
@@ -330,10 +329,10 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
|
||||
JsonObject root = builder.root();
|
||||
root[ESPHOME_F("name")] = group.second.name;
|
||||
root[ESPHOME_F("sorting_weight")] = group.second.weight;
|
||||
auto group_msg = builder.serialize();
|
||||
message = builder.serialize();
|
||||
|
||||
// up to 31 groups should be able to be queued initially without defer
|
||||
source->try_send_nodefer(group_msg.c_str(), "sorting_group");
|
||||
source->try_send_nodefer(message.c_str(), "sorting_group");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -366,7 +365,7 @@ void WebServer::set_css_include(const char *css_include) { this->css_include_ =
|
||||
void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
|
||||
#endif
|
||||
|
||||
json::SerializationBuffer<> WebServer::get_config_json() {
|
||||
std::string WebServer::get_config_json() {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -598,20 +597,20 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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;
|
||||
}
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -645,23 +644,23 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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;
|
||||
}
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -706,7 +705,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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;
|
||||
}
|
||||
@@ -735,13 +734,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::switch_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::switch_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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,7 +762,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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(ESPHOME_F("press"))) {
|
||||
DEFER_ACTION(obj, obj->press());
|
||||
@@ -776,10 +775,10 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::button_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -806,23 +805,22 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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;
|
||||
}
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -849,7 +847,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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(ESPHOME_F("toggle"))) {
|
||||
DEFER_ACTION(obj, obj->toggle().perform());
|
||||
@@ -890,13 +888,13 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -930,7 +928,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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(ESPHOME_F("toggle"))) {
|
||||
DEFER_ACTION(obj, obj->toggle().perform());
|
||||
@@ -969,13 +967,13 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::light_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->light_json_((light::LightState *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::light_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->light_json_((light::LightState *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1009,7 +1007,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->cover_json_(obj, detail);
|
||||
std::string data = this->cover_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1057,13 +1055,13 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1098,7 +1096,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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;
|
||||
}
|
||||
@@ -1117,13 +1115,13 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::number_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::number_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1165,7 +1163,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->date_json_(obj, detail);
|
||||
std::string data = this->date_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1190,13 +1188,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::date_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::date_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1225,7 +1223,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->time_json_(obj, detail);
|
||||
std::string data = this->time_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1249,13 +1247,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::time_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::time_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1284,7 +1282,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->datetime_json_(obj, detail);
|
||||
std::string data = this->datetime_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1308,13 +1306,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1345,7 +1343,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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;
|
||||
}
|
||||
@@ -1364,13 +1362,13 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::text_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::text_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1402,7 +1400,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
|
||||
std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1421,15 +1419,15 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::select_state_json_generator(WebServer *web_server, void *source) {
|
||||
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() : StringRef(), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::select_all_json_generator(WebServer *web_server, void *source) {
|
||||
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() : StringRef(), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
|
||||
std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1461,7 +1459,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->climate_json_(obj, detail);
|
||||
std::string data = this->climate_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1490,15 +1488,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::climate_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::climate_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
@@ -1631,7 +1629,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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;
|
||||
}
|
||||
@@ -1660,13 +1658,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::lock_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::lock_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1694,7 +1692,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->valve_json_(obj, detail);
|
||||
std::string data = this->valve_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1740,13 +1738,13 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1779,7 +1777,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto 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;
|
||||
}
|
||||
@@ -1819,19 +1817,19 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -1860,7 +1858,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->water_heater_json_(obj, detail);
|
||||
std::string data = this->water_heater_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1896,14 +1894,14 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
|
||||
std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
char buf[PSTR_LOCAL_SIZE];
|
||||
@@ -1966,7 +1964,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->infrared_json_(obj, detail);
|
||||
std::string data = this->infrared_json_(obj, detail);
|
||||
request->send(200, ESPHOME_F("application/json"), data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -2037,12 +2035,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
|
||||
std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -2077,7 +2075,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->event_json_(obj, StringRef(), detail);
|
||||
std::string data = this->event_json_(obj, StringRef(), detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -2087,16 +2085,16 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); }
|
||||
|
||||
json::SerializationBuffer<> WebServer::event_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
json::SerializationBuffer<> WebServer::event_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
|
||||
std::string WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -2143,7 +2141,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->update_json_(obj, detail);
|
||||
std::string data = this->update_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -2159,15 +2157,15 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> 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();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "list_entities.h"
|
||||
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/web_server_base/web_server_base.h"
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/core/component.h"
|
||||
@@ -105,7 +104,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
|
||||
can be forgotten.
|
||||
*/
|
||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||
using message_generator_t = json::SerializationBuffer<>(WebServer *, void *);
|
||||
using message_generator_t = std::string(WebServer *, void *);
|
||||
|
||||
class DeferredUpdateEventSourceList;
|
||||
class DeferredUpdateEventSource : public AsyncEventSource {
|
||||
@@ -264,7 +263,7 @@ class WebServer : public Controller,
|
||||
void handle_index_request(AsyncWebServerRequest *request);
|
||||
|
||||
/// Return the webserver configuration as JSON.
|
||||
json::SerializationBuffer<> get_config_json();
|
||||
std::string get_config_json();
|
||||
|
||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||
/// Handle included css request under '/0.css'.
|
||||
@@ -286,8 +285,8 @@ class WebServer : public Controller,
|
||||
/// Handle a sensor request under '/sensor/<id>'.
|
||||
void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
@@ -296,8 +295,8 @@ class WebServer : public Controller,
|
||||
/// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
|
||||
void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> switch_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> switch_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string switch_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string switch_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_BUTTON
|
||||
@@ -305,7 +304,7 @@ class WebServer : public Controller,
|
||||
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
// Buttons are stateless, so there is no button_state_json_generator
|
||||
static json::SerializationBuffer<> button_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string button_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -314,8 +313,8 @@ class WebServer : public Controller,
|
||||
/// Handle a binary sensor request under '/binary_sensor/<id>'.
|
||||
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> binary_sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> binary_sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
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);
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
@@ -324,8 +323,8 @@ class WebServer : public Controller,
|
||||
/// Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'.
|
||||
void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> fan_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> fan_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string fan_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string fan_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
@@ -334,8 +333,8 @@ class WebServer : public Controller,
|
||||
/// Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'.
|
||||
void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> light_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> light_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string light_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string light_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
@@ -344,8 +343,8 @@ class WebServer : public Controller,
|
||||
/// Handle a text sensor request under '/text_sensor/<id>'.
|
||||
void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> text_sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> text_sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
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);
|
||||
#endif
|
||||
|
||||
#ifdef USE_COVER
|
||||
@@ -354,8 +353,8 @@ class WebServer : public Controller,
|
||||
/// Handle a cover request under '/cover/<id>/<open/close/stop/set>'.
|
||||
void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> cover_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> cover_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string cover_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string cover_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
@@ -363,8 +362,8 @@ class WebServer : public Controller,
|
||||
/// Handle a number request under '/number/<id>'.
|
||||
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> number_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> number_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string number_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string number_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
@@ -372,8 +371,8 @@ class WebServer : public Controller,
|
||||
/// Handle a date request under '/date/<id>'.
|
||||
void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> date_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> date_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string date_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string date_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
@@ -381,8 +380,8 @@ class WebServer : public Controller,
|
||||
/// Handle a time request under '/time/<id>'.
|
||||
void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> time_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> time_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string time_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string time_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
@@ -390,8 +389,8 @@ class WebServer : public Controller,
|
||||
/// Handle a datetime request under '/datetime/<id>'.
|
||||
void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> datetime_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> datetime_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string datetime_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string datetime_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
@@ -399,8 +398,8 @@ class WebServer : public Controller,
|
||||
/// Handle a text input request under '/text/<id>'.
|
||||
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> text_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> text_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string text_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string text_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
@@ -408,8 +407,8 @@ class WebServer : public Controller,
|
||||
/// Handle a select request under '/select/<id>'.
|
||||
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> select_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> select_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string select_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string select_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
@@ -417,8 +416,8 @@ class WebServer : public Controller,
|
||||
/// Handle a climate request under '/climate/<id>'.
|
||||
void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> climate_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> climate_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string climate_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string climate_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOCK
|
||||
@@ -427,8 +426,8 @@ class WebServer : public Controller,
|
||||
/// Handle a lock request under '/lock/<id>/</lock/unlock/open>'.
|
||||
void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> lock_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> lock_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string lock_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string lock_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_VALVE
|
||||
@@ -437,8 +436,8 @@ class WebServer : public Controller,
|
||||
/// Handle a valve request under '/valve/<id>/<open/close/stop/set>'.
|
||||
void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> valve_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> valve_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string valve_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string valve_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
@@ -447,8 +446,8 @@ class WebServer : public Controller,
|
||||
/// Handle a alarm_control_panel request under '/alarm_control_panel/<id>'.
|
||||
void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> alarm_control_panel_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
|
||||
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);
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
@@ -457,22 +456,22 @@ class WebServer : public Controller,
|
||||
/// Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
|
||||
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> water_heater_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> water_heater_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string water_heater_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string water_heater_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
/// Handle an infrared request under '/infrared/<id>/transmit'.
|
||||
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string infrared_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj) override;
|
||||
|
||||
static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string event_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string event_all_json_generator(WebServer *web_server, void *source);
|
||||
|
||||
/// Handle a event request under '/event<id>'.
|
||||
void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
@@ -484,8 +483,8 @@ class WebServer : public Controller,
|
||||
/// Handle a update request under '/update/<id>'.
|
||||
void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> update_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> update_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string update_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string update_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
/// Override the web handler's canHandle method.
|
||||
@@ -610,74 +609,71 @@ class WebServer : public Controller,
|
||||
|
||||
private:
|
||||
#ifdef USE_SENSOR
|
||||
json::SerializationBuffer<> sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
|
||||
std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
json::SerializationBuffer<> switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
|
||||
std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
json::SerializationBuffer<> button_json_(button::Button *obj, JsonDetail start_config);
|
||||
std::string button_json_(button::Button *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
json::SerializationBuffer<> binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
|
||||
JsonDetail start_config);
|
||||
std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
json::SerializationBuffer<> fan_json_(fan::Fan *obj, JsonDetail start_config);
|
||||
std::string fan_json_(fan::Fan *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
json::SerializationBuffer<> light_json_(light::LightState *obj, JsonDetail start_config);
|
||||
std::string light_json_(light::LightState *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
json::SerializationBuffer<> text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
|
||||
JsonDetail start_config);
|
||||
std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
json::SerializationBuffer<> cover_json_(cover::Cover *obj, JsonDetail start_config);
|
||||
std::string cover_json_(cover::Cover *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
json::SerializationBuffer<> number_json_(number::Number *obj, float value, JsonDetail start_config);
|
||||
std::string number_json_(number::Number *obj, float value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
json::SerializationBuffer<> date_json_(datetime::DateEntity *obj, JsonDetail start_config);
|
||||
std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
json::SerializationBuffer<> time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
|
||||
std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
json::SerializationBuffer<> datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
|
||||
std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
json::SerializationBuffer<> text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
|
||||
std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
json::SerializationBuffer<> select_json_(select::Select *obj, StringRef value, JsonDetail start_config);
|
||||
std::string select_json_(select::Select *obj, StringRef value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
json::SerializationBuffer<> climate_json_(climate::Climate *obj, JsonDetail start_config);
|
||||
std::string climate_json_(climate::Climate *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
json::SerializationBuffer<> lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
|
||||
std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
json::SerializationBuffer<> valve_json_(valve::Valve *obj, JsonDetail start_config);
|
||||
std::string valve_json_(valve::Valve *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
json::SerializationBuffer<> alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
|
||||
alarm_control_panel::AlarmControlPanelState value,
|
||||
JsonDetail start_config);
|
||||
std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
|
||||
alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
json::SerializationBuffer<> event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config);
|
||||
std::string event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
json::SerializationBuffer<> water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
|
||||
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
|
||||
std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config);
|
||||
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -38,8 +38,11 @@ async def to_code(config):
|
||||
cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
|
||||
return
|
||||
|
||||
# ESP32 uses IDF web server (early return above), so this is for other Arduino platforms
|
||||
if CORE.using_arduino:
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("WiFi", None)
|
||||
cg.add_library("FS", None)
|
||||
cg.add_library("Update", None)
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266WiFi", None)
|
||||
if CORE.is_libretiny:
|
||||
|
||||
@@ -78,8 +78,11 @@ optional<std::string> query_key_value(const std::string &query_url, const std::s
|
||||
return {};
|
||||
}
|
||||
|
||||
// Use stack buffer for typical query strings, heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256, char> val(query_url.size());
|
||||
auto val = std::unique_ptr<char[]>(new char[query_url.size()]);
|
||||
if (!val) {
|
||||
ESP_LOGE(TAG, "Not enough memory to the query key value");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (httpd_query_key_value(query_url.c_str(), key.c_str(), val.get(), query_url.size()) != ESP_OK) {
|
||||
return {};
|
||||
|
||||
@@ -347,15 +347,14 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
|
||||
memcpy(user_info + user_len + 1, password, pass_len);
|
||||
user_info[user_info_len] = '\0';
|
||||
|
||||
// Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes
|
||||
// max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety
|
||||
constexpr size_t max_digest_len = 350;
|
||||
char digest[max_digest_len];
|
||||
size_t out;
|
||||
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
|
||||
size_t n = 0, out;
|
||||
esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast<const uint8_t *>(user_info), user_info_len);
|
||||
|
||||
auto digest = std::unique_ptr<char[]>(new char[n + 1]);
|
||||
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out,
|
||||
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
|
||||
|
||||
return strcmp(digest, auth_str + auth_prefix_len) == 0;
|
||||
return strcmp(digest.get(), auth_str + auth_prefix_len) == 0;
|
||||
}
|
||||
|
||||
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
|
||||
@@ -402,9 +401,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);
|
||||
}
|
||||
|
||||
@@ -509,7 +507,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
|
||||
// Configure reconnect timeout and send config
|
||||
// this should always go through since the tcp send buffer is empty on connect
|
||||
auto message = ws->get_config_json();
|
||||
std::string message = ws->get_config_json();
|
||||
this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
@@ -563,7 +561,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g
|
||||
void AsyncEventSourceResponse::process_deferred_queue_() {
|
||||
while (!deferred_queue_.empty()) {
|
||||
DeferredEvent &de = deferred_queue_.front();
|
||||
auto message = de.message_generator_(web_server_, de.source_);
|
||||
std::string message = de.message_generator_(web_server_, de.source_);
|
||||
if (this->try_send_nodefer(message.c_str(), "state")) {
|
||||
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
|
||||
deferred_queue_.erase(deferred_queue_.begin());
|
||||
@@ -800,7 +798,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
|
||||
// trying to send first
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
} else {
|
||||
auto message = message_generator(web_server_, source);
|
||||
std::string message = message_generator(web_server_, source);
|
||||
if (!this->try_send_nodefer(message.c_str(), "state")) {
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
}
|
||||
@@ -866,12 +864,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
|
||||
}
|
||||
});
|
||||
|
||||
// Process data - use stack buffer to avoid heap allocation
|
||||
char buffer[MULTIPART_CHUNK_SIZE];
|
||||
// Process data
|
||||
std::unique_ptr<char[]> buffer(new char[MULTIPART_CHUNK_SIZE]);
|
||||
size_t bytes_since_yield = 0;
|
||||
|
||||
for (size_t remaining = r->content_len; remaining > 0;) {
|
||||
int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE));
|
||||
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
|
||||
|
||||
if (recv_len <= 0) {
|
||||
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
|
||||
@@ -879,7 +877,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
|
||||
return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
|
||||
}
|
||||
|
||||
if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) {
|
||||
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
|
||||
ESP_LOGW(TAG, "Multipart parser error");
|
||||
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
|
||||
return ESP_FAIL;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/web_server/list_entities.h"
|
||||
#endif
|
||||
|
||||
@@ -251,7 +250,7 @@ class AsyncWebHandler {
|
||||
class AsyncEventSource;
|
||||
class AsyncEventSourceResponse;
|
||||
|
||||
using message_generator_t = json::SerializationBuffer<>(esphome::web_server::WebServer *, void *);
|
||||
using message_generator_t = std::string(esphome::web_server::WebServer *, void *);
|
||||
|
||||
/*
|
||||
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
|
||||
|
||||
@@ -351,7 +351,7 @@ bool WiFiComponent::needs_scan_results_() const {
|
||||
return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
|
||||
}
|
||||
|
||||
bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const {
|
||||
bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
|
||||
// Check if this SSID is configured as hidden
|
||||
// If explicitly marked hidden, we should always try hidden mode regardless of scan results
|
||||
for (const auto &conf : this->sta_) {
|
||||
@@ -643,7 +643,7 @@ void WiFiComponent::restart_adapter() {
|
||||
// through start_connecting() first. Without this clear, stale errors would
|
||||
// trigger spurious "failed (callback)" logs. The canonical clear location
|
||||
// is in start_connecting(); this is the only exception to that pattern.
|
||||
this->error_from_callback_ = false;
|
||||
this->error_from_callback_ = 0;
|
||||
}
|
||||
|
||||
void WiFiComponent::loop() {
|
||||
@@ -955,12 +955,9 @@ WiFiAP WiFiComponent::get_sta() const {
|
||||
return config ? *config : WiFiAP{};
|
||||
}
|
||||
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
|
||||
this->save_wifi_sta(ssid.c_str(), password.c_str());
|
||||
}
|
||||
void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
|
||||
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
|
||||
strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
|
||||
strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
|
||||
strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
|
||||
strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
|
||||
this->pref_.save(&save);
|
||||
// ensure it's written immediately
|
||||
global_preferences->sync();
|
||||
@@ -1066,7 +1063,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
// This is the canonical location for clearing the flag since all connection
|
||||
// attempts go through start_connecting(). The only other clear is in
|
||||
// restart_adapter() which enters COOLDOWN without calling start_connecting().
|
||||
this->error_from_callback_ = false;
|
||||
this->error_from_callback_ = 0;
|
||||
|
||||
if (!this->wifi_sta_connect_(ap)) {
|
||||
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
||||
@@ -1471,7 +1468,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
}
|
||||
|
||||
if (this->error_from_callback_) {
|
||||
// ESP8266: logging done in callback, listeners deferred via pending_.disconnect
|
||||
// Other platforms: just log generic failure message
|
||||
#ifndef USE_ESP8266
|
||||
ESP_LOGW(TAG, "Connecting to network failed (callback)");
|
||||
#endif
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
@@ -1805,11 +1806,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
}
|
||||
|
||||
// Get SSID for logging (use pointer to avoid copy)
|
||||
const char *ssid = nullptr;
|
||||
const std::string *ssid = nullptr;
|
||||
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
|
||||
ssid = this->scan_result_[0].get_ssid().c_str();
|
||||
ssid = &this->scan_result_[0].get_ssid();
|
||||
} else if (const WiFiAP *config = this->get_selected_sta_()) {
|
||||
ssid = config->get_ssid().c_str();
|
||||
ssid = &config->get_ssid();
|
||||
}
|
||||
|
||||
// Only decrease priority on the last attempt for this phase
|
||||
@@ -1827,11 +1828,10 @@ 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 != nullptr ? ssid : "",
|
||||
bssid_s, old_priority, new_priority);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
|
||||
ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
|
||||
|
||||
// After adjusting priority, check if all priorities are now at minimum
|
||||
// If so, clear the vector to save memory and reset for fresh start
|
||||
@@ -2079,14 +2079,10 @@ void WiFiComponent::save_fast_connect_settings_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
|
||||
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
|
||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
|
||||
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
|
||||
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
|
||||
void WiFiAP::set_password(const std::string &password) {
|
||||
this->password_ = CompactString(password.c_str(), password.size());
|
||||
}
|
||||
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
|
||||
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
|
||||
#endif
|
||||
@@ -2096,8 +2092,10 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
|
||||
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
|
||||
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
|
||||
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
|
||||
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
|
||||
const std::string &WiFiAP::get_password() const { return this->password_; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
|
||||
#endif
|
||||
@@ -2108,12 +2106,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip
|
||||
#endif
|
||||
bool WiFiAP::get_hidden() const { return this->hidden_; }
|
||||
|
||||
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
|
||||
bool with_auth, bool is_hidden)
|
||||
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
|
||||
bool is_hidden)
|
||||
: bssid_(bssid),
|
||||
channel_(channel),
|
||||
rssi_(rssi),
|
||||
ssid_(ssid, ssid_len),
|
||||
ssid_(std::move(ssid)),
|
||||
with_auth_(with_auth),
|
||||
is_hidden_(is_hidden) {}
|
||||
bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
@@ -2156,6 +2154,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
bool WiFiScanResult::get_matches() const { return this->matches_; }
|
||||
void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
|
||||
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
|
||||
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
|
||||
uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
|
||||
int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
|
||||
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
|
||||
@@ -2228,7 +2227,7 @@ void WiFiComponent::process_roaming_scan_() {
|
||||
|
||||
for (const auto &result : this->scan_result_) {
|
||||
// Must be same SSID, different BSSID
|
||||
if (result.get_ssid() != current_ssid.c_str() || result.get_bssid() == current_bssid)
|
||||
if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid)
|
||||
continue;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
|
||||
@@ -58,6 +58,12 @@ static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127;
|
||||
/// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator)
|
||||
static constexpr size_t SSID_BUFFER_SIZE = 33;
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
/// Special disconnect reason for authmode downgrade (CVE-2020-12638 mitigation)
|
||||
/// Not a real WiFi reason code - used internally for deferred logging
|
||||
static constexpr uint8_t WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE = 254;
|
||||
#endif
|
||||
|
||||
struct SavedWifiSettings {
|
||||
char ssid[33];
|
||||
char password[65];
|
||||
@@ -175,13 +181,9 @@ template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||
class WiFiAP {
|
||||
public:
|
||||
void set_ssid(const std::string &ssid);
|
||||
void set_ssid(const char *ssid);
|
||||
void set_ssid(const CompactString &ssid) { this->ssid_ = ssid; }
|
||||
void set_bssid(const bssid_t &bssid);
|
||||
void clear_bssid();
|
||||
void set_password(const std::string &password);
|
||||
void set_password(const char *password);
|
||||
void set_password(const CompactString &password) { this->password_ = password; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
void set_eap(optional<EAPAuth> eap_auth);
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -192,10 +194,10 @@ class WiFiAP {
|
||||
void set_manual_ip(optional<ManualIP> manual_ip);
|
||||
#endif
|
||||
void set_hidden(bool hidden);
|
||||
const CompactString &get_ssid() const { return this->ssid_; }
|
||||
const CompactString &get_password() const { return this->password_; }
|
||||
const std::string &get_ssid() const;
|
||||
const bssid_t &get_bssid() const;
|
||||
bool has_bssid() const;
|
||||
const std::string &get_password() const;
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
const optional<EAPAuth> &get_eap() const;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -208,8 +210,8 @@ class WiFiAP {
|
||||
bool get_hidden() const;
|
||||
|
||||
protected:
|
||||
CompactString ssid_;
|
||||
CompactString password_;
|
||||
std::string ssid_;
|
||||
std::string password_;
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
optional<EAPAuth> eap_;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -225,15 +227,14 @@ class WiFiAP {
|
||||
|
||||
class WiFiScanResult {
|
||||
public:
|
||||
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth,
|
||||
bool is_hidden);
|
||||
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
|
||||
|
||||
bool matches(const WiFiAP &config) const;
|
||||
|
||||
bool get_matches() const;
|
||||
void set_matches(bool matches);
|
||||
const bssid_t &get_bssid() const;
|
||||
const CompactString &get_ssid() const { return this->ssid_; }
|
||||
const std::string &get_ssid() const;
|
||||
uint8_t get_channel() const;
|
||||
int8_t get_rssi() const;
|
||||
bool get_with_auth() const;
|
||||
@@ -247,7 +248,7 @@ class WiFiScanResult {
|
||||
bssid_t bssid_;
|
||||
uint8_t channel_;
|
||||
int8_t rssi_;
|
||||
CompactString ssid_;
|
||||
std::string ssid_;
|
||||
int8_t priority_{0};
|
||||
bool matches_{false};
|
||||
bool with_auth_;
|
||||
@@ -386,10 +387,6 @@ class WiFiComponent : public Component {
|
||||
void set_passive_scan(bool passive);
|
||||
|
||||
void save_wifi_sta(const std::string &ssid, const std::string &password);
|
||||
void save_wifi_sta(const char *ssid, const char *password);
|
||||
void save_wifi_sta(const CompactString &ssid, const CompactString &password) {
|
||||
this->save_wifi_sta(ssid.c_str(), password.c_str());
|
||||
}
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
@@ -550,7 +547,7 @@ class WiFiComponent : public Component {
|
||||
int8_t find_first_non_hidden_index_() const;
|
||||
/// Check if an SSID was seen in the most recent scan results
|
||||
/// Used to skip hidden mode for SSIDs we know are visible
|
||||
bool ssid_was_seen_in_scan_(const CompactString &ssid) const;
|
||||
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
|
||||
/// Check if full scan results are needed (captive portal active, improv, listeners)
|
||||
bool needs_full_scan_results_() const;
|
||||
/// Check if network matches any configured network (for scan result filtering)
|
||||
@@ -599,6 +596,9 @@ class WiFiComponent : public Component {
|
||||
void connect_soon_();
|
||||
|
||||
void wifi_loop_();
|
||||
#ifdef USE_ESP8266
|
||||
void process_pending_callbacks_();
|
||||
#endif
|
||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||
bool wifi_sta_pre_setup_();
|
||||
bool wifi_apply_output_power_(float output_power);
|
||||
@@ -713,10 +713,26 @@ class WiFiComponent : public Component {
|
||||
uint8_t num_ipv6_addresses_{0};
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
// 0 = no error, non-zero = disconnect reason code from callback
|
||||
// This serves as both the error flag and stores the reason for deferred logging
|
||||
uint8_t error_from_callback_{0};
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// Pending listener callbacks from system context (ESP8266 only)
|
||||
// ESP8266 callbacks run in SDK system context with ~2KB stack where
|
||||
// calling arbitrary listener callbacks is unsafe. These flags defer
|
||||
// listener notifications to wifi_loop_() which runs with full stack.
|
||||
struct {
|
||||
bool connect : 1; // STA connected, notify listeners
|
||||
bool disconnect : 1; // STA disconnected, notify listeners
|
||||
bool got_ip : 1; // Got IP, notify listeners
|
||||
bool scan_complete : 1; // Scan complete, notify listeners
|
||||
} pending_{};
|
||||
#endif
|
||||
|
||||
// Group all boolean values together
|
||||
bool has_ap_{false};
|
||||
bool handled_connected_state_{false};
|
||||
bool error_from_callback_{false};
|
||||
bool scan_done_{false};
|
||||
bool ap_setup_{false};
|
||||
bool ap_started_{false};
|
||||
|
||||
@@ -36,7 +36,6 @@ extern "C" {
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/util.h"
|
||||
|
||||
namespace esphome::wifi {
|
||||
@@ -399,22 +398,36 @@ class WiFiMockClass : public ESP8266WiFiGenericClass {
|
||||
static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT
|
||||
};
|
||||
|
||||
// Auth mode strings indexed by AUTH_* constants (0-4), with UNKNOWN at last index
|
||||
// Static asserts verify the SDK constants are contiguous as expected
|
||||
static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4,
|
||||
"AUTH_* constants are not contiguous");
|
||||
PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN");
|
||||
|
||||
const LogString *get_auth_mode_str(uint8_t mode) {
|
||||
return AuthModeStrings::get_log_str(mode, AuthModeStrings::LAST_INDEX);
|
||||
switch (mode) {
|
||||
case AUTH_OPEN:
|
||||
return LOG_STR("OPEN");
|
||||
case AUTH_WEP:
|
||||
return LOG_STR("WEP");
|
||||
case AUTH_WPA_PSK:
|
||||
return LOG_STR("WPA PSK");
|
||||
case AUTH_WPA2_PSK:
|
||||
return LOG_STR("WPA2 PSK");
|
||||
case AUTH_WPA_WPA2_PSK:
|
||||
return LOG_STR("WPA/WPA2 PSK");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
const LogString *get_op_mode_str(uint8_t mode) {
|
||||
switch (mode) {
|
||||
case WIFI_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case WIFI_STA:
|
||||
return LOG_STR("STA");
|
||||
case WIFI_AP:
|
||||
return LOG_STR("AP");
|
||||
case WIFI_AP_STA:
|
||||
return LOG_STR("AP+STA");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at last index
|
||||
static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3,
|
||||
"WIFI_* op mode constants are not contiguous");
|
||||
PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN");
|
||||
|
||||
const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); }
|
||||
|
||||
const LogString *get_disconnect_reason_str(uint8_t reason) {
|
||||
/* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the
|
||||
@@ -498,21 +511,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
it.channel);
|
||||
#endif
|
||||
s_sta_connected = true;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
for (auto *listener : global_wifi_component->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
|
||||
}
|
||||
#endif
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
if (const WiFiAP *config = global_wifi_component->get_selected_sta_();
|
||||
config && config->get_manual_ip().has_value()) {
|
||||
for (auto *listener : global_wifi_component->ip_state_listeners_) {
|
||||
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(),
|
||||
global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Defer listener callbacks to main loop - system context has limited stack
|
||||
global_wifi_component->pending_.connect = true;
|
||||
break;
|
||||
}
|
||||
case EVENT_STAMODE_DISCONNECTED: {
|
||||
@@ -530,17 +530,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
}
|
||||
s_sta_connected = false;
|
||||
s_sta_connecting = false;
|
||||
// IMPORTANT: Set error flag BEFORE notifying listeners.
|
||||
// This ensures is_connected() returns false during listener callbacks,
|
||||
// which is critical for proper reconnection logic (e.g., roaming).
|
||||
global_wifi_component->error_from_callback_ = true;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
// Notify listeners AFTER setting error flag so they see correct state
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : global_wifi_component->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
|
||||
}
|
||||
#endif
|
||||
// Store reason as error flag; defer listener callbacks to main loop
|
||||
global_wifi_component->error_from_callback_ = it.reason;
|
||||
global_wifi_component->pending_.disconnect = true;
|
||||
break;
|
||||
}
|
||||
case EVENT_STAMODE_AUTHMODE_CHANGE: {
|
||||
@@ -551,10 +543,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
|
||||
if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) {
|
||||
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
|
||||
// we can't call retry_connect() from this context, so disconnect immediately
|
||||
// and notify main thread with error_from_callback_
|
||||
wifi_station_disconnect();
|
||||
global_wifi_component->error_from_callback_ = true;
|
||||
global_wifi_component->error_from_callback_ = WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -565,12 +555,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf),
|
||||
network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf));
|
||||
s_sta_got_ip = true;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : global_wifi_component->ip_state_listeners_) {
|
||||
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0),
|
||||
global_wifi_component->get_dns_address(1));
|
||||
}
|
||||
#endif
|
||||
// Defer listener callbacks to main loop - system context has limited stack
|
||||
global_wifi_component->pending_.got_ip = true;
|
||||
break;
|
||||
}
|
||||
case EVENT_STAMODE_DHCP_TIMEOUT: {
|
||||
@@ -771,8 +757,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
|
||||
this->scan_result_.emplace_back(
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
|
||||
it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
|
||||
}
|
||||
@@ -780,11 +766,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
this->scan_done_ = true;
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : global_wifi_component->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(global_wifi_component->scan_result_);
|
||||
}
|
||||
#endif
|
||||
this->pending_.scan_complete = true; // Defer listener callbacks to main loop
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
@@ -970,7 +952,59 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() {
|
||||
return network::IPAddress(&ip.gw);
|
||||
}
|
||||
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); }
|
||||
void WiFiComponent::wifi_loop_() {}
|
||||
void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); }
|
||||
|
||||
void WiFiComponent::process_pending_callbacks_() {
|
||||
// Notify listeners for connect event (logging already done in callback)
|
||||
if (this->pending_.connect) {
|
||||
this->pending_.connect = false;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
bssid_t bssid = this->wifi_bssid();
|
||||
char ssid_buf[SSID_BUFFER_SIZE];
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(this->wifi_ssid_to(ssid_buf)), bssid);
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Notify listeners for disconnect event (logging already done in callback)
|
||||
if (this->pending_.disconnect) {
|
||||
this->pending_.disconnect = false;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Notify listeners for got IP event (logging already done in callback)
|
||||
if (this->pending_.got_ip) {
|
||||
this->pending_.got_ip = false;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Notify listeners for scan complete (logging already done in callback)
|
||||
if (this->pending_.scan_complete) {
|
||||
this->pending_.scan_complete = false;
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::wifi
|
||||
#endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user