mirror of
https://github.com/esphome/esphome.git
synced 2026-02-03 02:07:41 -07:00
Compare commits
1 Commits
integratio
...
json_web_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
840ad30880 |
@@ -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
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -134,7 +134,6 @@ esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
esphome/components/dht/* @OttoWinter
|
||||
esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dlms_meter/* @SimonFischer04
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
|
||||
@@ -4,9 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import heapq
|
||||
import json
|
||||
from operator import itemgetter
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -32,10 +29,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
)
|
||||
# Lower threshold for RAM symbols (RAM is more constrained)
|
||||
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
|
||||
# Number of top symbols to show in the largest symbols report
|
||||
TOP_SYMBOLS_LIMIT: int = 30
|
||||
# Width for symbol name display in top symbols report
|
||||
COL_TOP_SYMBOL_NAME: int = 55
|
||||
|
||||
# Column width constants
|
||||
COL_COMPONENT: int = 29
|
||||
@@ -154,37 +147,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
|
||||
return f"{demangled} ({size:,} B){section_label}"
|
||||
|
||||
def _add_top_symbols(self, lines: list[str]) -> None:
|
||||
"""Add a section showing the top largest symbols in the binary."""
|
||||
# Collect all symbols from all components: (symbol, demangled, size, section, component)
|
||||
all_symbols = [
|
||||
(symbol, demangled, size, section, component)
|
||||
for component, symbols in self._component_symbols.items()
|
||||
for symbol, demangled, size, section in symbols
|
||||
]
|
||||
|
||||
# Get top N symbols by size using heapq for efficiency
|
||||
top_symbols = heapq.nlargest(
|
||||
self.TOP_SYMBOLS_LIMIT, all_symbols, key=itemgetter(2)
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"Top {self.TOP_SYMBOLS_LIMIT} Largest Symbols:")
|
||||
# Calculate truncation limit from column width (leaving room for "...")
|
||||
truncate_limit = self.COL_TOP_SYMBOL_NAME - 3
|
||||
for i, (_, demangled, size, section, component) in enumerate(top_symbols):
|
||||
# Format section label
|
||||
section_label = f"[{section[1:]}]" if section else ""
|
||||
# Truncate demangled name if too long
|
||||
demangled_display = (
|
||||
f"{demangled[:truncate_limit]}..."
|
||||
if len(demangled) > self.COL_TOP_SYMBOL_NAME
|
||||
else demangled
|
||||
)
|
||||
lines.append(
|
||||
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
|
||||
)
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -286,9 +248,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
"RAM",
|
||||
)
|
||||
|
||||
# Top largest symbols in the binary
|
||||
self._add_top_symbols(lines)
|
||||
|
||||
# Add ESPHome core detailed analysis if there are core symbols
|
||||
if self._esphome_core_symbols:
|
||||
self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis")
|
||||
@@ -479,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());
|
||||
@@ -215,7 +211,7 @@ void APIServer::loop() {
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
this->client_disconnected_trigger_->trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
|
||||
@@ -227,10 +227,12 @@ class APIServer : public Component,
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *get_client_connected_trigger() { return &this->client_connected_trigger_; }
|
||||
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; }
|
||||
Trigger<std::string, std::string> *get_client_disconnected_trigger() const {
|
||||
return this->client_disconnected_trigger_;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
@@ -251,10 +253,10 @@ class APIServer : public Component,
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> client_connected_trigger_;
|
||||
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> client_disconnected_trigger_;
|
||||
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
|
||||
#endif
|
||||
|
||||
// 4-byte aligned types
|
||||
|
||||
@@ -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) {
|
||||
@@ -138,10 +136,12 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
void set_wants_response() { this->flags_.wants_response = true; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() { return &this->success_trigger_with_response_; }
|
||||
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
|
||||
return this->success_trigger_with_response_;
|
||||
}
|
||||
#endif
|
||||
Trigger<Ts...> *get_success_trigger() { return &this->success_trigger_; }
|
||||
Trigger<std::string, Ts...> *get_error_trigger() { return &this->error_trigger_; }
|
||||
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
|
||||
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
@@ -187,14 +187,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
if (response.is_success()) {
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
if (this->flags_.wants_response) {
|
||||
this->success_trigger_with_response_.trigger(response.get_json(), args...);
|
||||
this->success_trigger_with_response_->trigger(response.get_json(), args...);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->success_trigger_.trigger(args...);
|
||||
this->success_trigger_->trigger(args...);
|
||||
}
|
||||
} else {
|
||||
this->error_trigger_.trigger(response.get_error_message(), args...);
|
||||
this->error_trigger_->trigger(response.get_error_message(), args...);
|
||||
}
|
||||
},
|
||||
captured_args);
|
||||
@@ -251,10 +251,10 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
TemplatableStringValue<Ts...> response_template_{""};
|
||||
Trigger<JsonObjectConst, Ts...> success_trigger_with_response_;
|
||||
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
Trigger<Ts...> success_trigger_;
|
||||
Trigger<std::string, Ts...> error_trigger_;
|
||||
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
|
||||
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
|
||||
struct Flags {
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace bang_bang {
|
||||
|
||||
static const char *const TAG = "bang_bang.climate";
|
||||
|
||||
BangBangClimate::BangBangClimate() = default;
|
||||
BangBangClimate::BangBangClimate()
|
||||
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
|
||||
|
||||
void BangBangClimate::setup() {
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
@@ -159,13 +160,13 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
||||
switch (action) {
|
||||
case climate::CLIMATE_ACTION_OFF:
|
||||
case climate::CLIMATE_ACTION_IDLE:
|
||||
trig = &this->idle_trigger_;
|
||||
trig = this->idle_trigger_;
|
||||
break;
|
||||
case climate::CLIMATE_ACTION_COOLING:
|
||||
trig = &this->cool_trigger_;
|
||||
trig = this->cool_trigger_;
|
||||
break;
|
||||
case climate::CLIMATE_ACTION_HEATING:
|
||||
trig = &this->heat_trigger_;
|
||||
trig = this->heat_trigger_;
|
||||
break;
|
||||
default:
|
||||
trig = nullptr;
|
||||
@@ -203,9 +204,9 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
|
||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
||||
|
||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
|
||||
@@ -30,9 +30,9 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||
|
||||
Trigger<> *get_idle_trigger();
|
||||
Trigger<> *get_cool_trigger();
|
||||
Trigger<> *get_heat_trigger();
|
||||
Trigger<> *get_idle_trigger() const;
|
||||
Trigger<> *get_cool_trigger() const;
|
||||
Trigger<> *get_heat_trigger() const;
|
||||
|
||||
protected:
|
||||
/// Override control to change settings of the climate device.
|
||||
@@ -57,13 +57,17 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
*
|
||||
* In idle mode, the controller is assumed to have both heating and cooling disabled.
|
||||
*/
|
||||
Trigger<> idle_trigger_;
|
||||
Trigger<> *idle_trigger_{nullptr};
|
||||
/** The trigger to call when the controller should switch to cooling mode.
|
||||
*/
|
||||
Trigger<> cool_trigger_;
|
||||
Trigger<> *cool_trigger_{nullptr};
|
||||
/** The trigger to call when the controller should switch to heating mode.
|
||||
*
|
||||
* A null value for this attribute means that the controller has no heating action
|
||||
* For example window blinds, where only cooling (blinds closed) and not-cooling
|
||||
* (blinds open) is possible.
|
||||
*/
|
||||
Trigger<> heat_trigger_;
|
||||
Trigger<> *heat_trigger_{nullptr};
|
||||
/** A reference to the trigger that was previously active.
|
||||
*
|
||||
* This is so that the previous trigger can be stopped before enabling a new one.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -156,7 +156,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
|
||||
for (auto &listener : this->listeners_) {
|
||||
listener->on_packet(packet, freq_offset, rssi, lqi);
|
||||
}
|
||||
this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi);
|
||||
this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi);
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
|
||||
@@ -79,7 +79,7 @@ class CC1101Component : public Component,
|
||||
// Packet mode operations
|
||||
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
||||
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
uint16_t chip_id_{0};
|
||||
@@ -96,7 +96,8 @@ class CC1101Component : public Component,
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> packet_trigger_;
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
|
||||
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
|
||||
std::vector<uint8_t> packet_;
|
||||
std::vector<CC1101Listener *> listeners_;
|
||||
|
||||
|
||||
@@ -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} {}
|
||||
|
||||
@@ -152,10 +152,6 @@ void CSE7766Component::parse_data_() {
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
} else if (this->power_sensor_ != nullptr) {
|
||||
// No valid power measurement from chip - publish 0W to avoid stale readings
|
||||
// This typically happens when current is below the measurable threshold (~50mA)
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
|
||||
float current = 0.0f;
|
||||
|
||||
@@ -66,7 +66,7 @@ void CurrentBasedCover::loop() {
|
||||
if (this->current_operation == COVER_OPERATION_OPENING) {
|
||||
if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction
|
||||
this->direction_idle_();
|
||||
this->malfunction_trigger_.trigger();
|
||||
this->malfunction_trigger_->trigger();
|
||||
ESP_LOGI(TAG, "'%s' - Malfunction detected during opening. Current flow detected in close circuit",
|
||||
this->name_.c_str());
|
||||
} else if (this->is_opening_blocked_()) { // Blocked
|
||||
@@ -87,7 +87,7 @@ void CurrentBasedCover::loop() {
|
||||
} else if (this->current_operation == COVER_OPERATION_CLOSING) {
|
||||
if (this->malfunction_detection_ && this->is_opening_()) { // Malfunction
|
||||
this->direction_idle_();
|
||||
this->malfunction_trigger_.trigger();
|
||||
this->malfunction_trigger_->trigger();
|
||||
ESP_LOGI(TAG, "'%s' - Malfunction detected during closing. Current flow detected in open circuit",
|
||||
this->name_.c_str());
|
||||
} else if (this->is_closing_blocked_()) { // Blocked
|
||||
@@ -221,15 +221,15 @@ void CurrentBasedCover::start_direction_(CoverOperation dir) {
|
||||
Trigger<> *trig;
|
||||
switch (dir) {
|
||||
case COVER_OPERATION_IDLE:
|
||||
trig = &this->stop_trigger_;
|
||||
trig = this->stop_trigger_;
|
||||
break;
|
||||
case COVER_OPERATION_OPENING:
|
||||
this->last_operation_ = dir;
|
||||
trig = &this->open_trigger_;
|
||||
trig = this->open_trigger_;
|
||||
break;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
this->last_operation_ = dir;
|
||||
trig = &this->close_trigger_;
|
||||
trig = this->close_trigger_;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
||||
@@ -16,9 +16,9 @@ class CurrentBasedCover : public cover::Cover, public Component {
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
|
||||
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
|
||||
|
||||
Trigger<> *get_open_trigger() { return &this->open_trigger_; }
|
||||
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
|
||||
void set_open_sensor(sensor::Sensor *open_sensor) { this->open_sensor_ = open_sensor; }
|
||||
void set_open_moving_current_threshold(float open_moving_current_threshold) {
|
||||
this->open_moving_current_threshold_ = open_moving_current_threshold;
|
||||
@@ -28,7 +28,7 @@ class CurrentBasedCover : public cover::Cover, public Component {
|
||||
}
|
||||
void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
|
||||
|
||||
Trigger<> *get_close_trigger() { return &this->close_trigger_; }
|
||||
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
|
||||
void set_close_sensor(sensor::Sensor *close_sensor) { this->close_sensor_ = close_sensor; }
|
||||
void set_close_moving_current_threshold(float close_moving_current_threshold) {
|
||||
this->close_moving_current_threshold_ = close_moving_current_threshold;
|
||||
@@ -44,7 +44,7 @@ class CurrentBasedCover : public cover::Cover, public Component {
|
||||
void set_malfunction_detection(bool malfunction_detection) { this->malfunction_detection_ = malfunction_detection; }
|
||||
void set_start_sensing_delay(uint32_t start_sensing_delay) { this->start_sensing_delay_ = start_sensing_delay; }
|
||||
|
||||
Trigger<> *get_malfunction_trigger() { return &this->malfunction_trigger_; }
|
||||
Trigger<> *get_malfunction_trigger() const { return this->malfunction_trigger_; }
|
||||
|
||||
cover::CoverTraits get_traits() override;
|
||||
|
||||
@@ -64,23 +64,23 @@ class CurrentBasedCover : public cover::Cover, public Component {
|
||||
|
||||
void recompute_position_();
|
||||
|
||||
Trigger<> stop_trigger_;
|
||||
Trigger<> *stop_trigger_{new Trigger<>()};
|
||||
|
||||
sensor::Sensor *open_sensor_{nullptr};
|
||||
Trigger<> open_trigger_;
|
||||
Trigger<> *open_trigger_{new Trigger<>()};
|
||||
float open_moving_current_threshold_;
|
||||
float open_obstacle_current_threshold_{FLT_MAX};
|
||||
uint32_t open_duration_;
|
||||
|
||||
sensor::Sensor *close_sensor_{nullptr};
|
||||
Trigger<> close_trigger_;
|
||||
Trigger<> *close_trigger_{new Trigger<>()};
|
||||
float close_moving_current_threshold_;
|
||||
float close_obstacle_current_threshold_{FLT_MAX};
|
||||
uint32_t close_duration_;
|
||||
|
||||
uint32_t max_duration_{UINT32_MAX};
|
||||
bool malfunction_detection_{true};
|
||||
Trigger<> malfunction_trigger_;
|
||||
Trigger<> *malfunction_trigger_{new Trigger<>()};
|
||||
uint32_t start_sensing_delay_;
|
||||
float obstacle_rollback_;
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
|
||||
|
||||
CODEOWNERS = ["@SimonFischer04"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_DLMS_METER_ID = "dlms_meter_id"
|
||||
CONF_DECRYPTION_KEY = "decryption_key"
|
||||
CONF_PROVIDER = "provider"
|
||||
|
||||
PROVIDERS = {"generic": 0, "netznoe": 1}
|
||||
|
||||
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
||||
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
||||
"DlmsMeterComponent", cg.Component, uart.UARTDevice
|
||||
)
|
||||
|
||||
|
||||
def validate_key(value):
|
||||
value = cv.string_strict(value)
|
||||
if len(value) != 32:
|
||||
raise cv.Invalid("Decryption key must be 32 hex characters (16 bytes)")
|
||||
try:
|
||||
return [int(value[i : i + 2], 16) for i in range(0, 32, 2)]
|
||||
except ValueError as exc:
|
||||
raise cv.Invalid("Decryption key must be hex values from 00 to FF") from exc
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_DECRYPTION_KEY): validate_key,
|
||||
cv.Optional(CONF_PROVIDER, default="generic"): cv.enum(
|
||||
PROVIDERS, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32]),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"dlms_meter", baud_rate=2400, require_rx=True
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
key = ", ".join(str(b) for b in config[CONF_DECRYPTION_KEY])
|
||||
cg.add(var.set_decryption_key(cg.RawExpression(f"{{{key}}}")))
|
||||
cg.add(var.set_provider(PROVIDERS[config[CONF_PROVIDER]]))
|
||||
@@ -1,71 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
/*
|
||||
+-------------------------------+
|
||||
| Ciphering Service |
|
||||
+-------------------------------+
|
||||
| System Title Length |
|
||||
+-------------------------------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| System |
|
||||
| Title |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+-------------------------------+
|
||||
| Length | (1 or 3 Bytes)
|
||||
+-------------------------------+
|
||||
| Security Control Byte |
|
||||
+-------------------------------+
|
||||
| |
|
||||
| Frame |
|
||||
| Counter |
|
||||
| |
|
||||
+-------------------------------+
|
||||
| |
|
||||
~ ~
|
||||
Encrypted Payload
|
||||
~ ~
|
||||
| |
|
||||
+-------------------------------+
|
||||
|
||||
Ciphering Service: 0xDB (General-Glo-Ciphering)
|
||||
System Title Length: 0x08
|
||||
System Title: Unique ID of meter
|
||||
Length: 1 Byte=Length <= 127, 3 Bytes=Length > 127 (0x82 & 2 Bytes length)
|
||||
Security Control Byte:
|
||||
- Bit 3…0: Security_Suite_Id
|
||||
- Bit 4: "A" subfield: indicates that authentication is applied
|
||||
- Bit 5: "E" subfield: indicates that encryption is applied
|
||||
- Bit 6: Key_Set subfield: 0 = Unicast, 1 = Broadcast
|
||||
- Bit 7: Indicates the use of compression.
|
||||
*/
|
||||
|
||||
static constexpr uint8_t DLMS_HEADER_LENGTH = 16;
|
||||
static constexpr uint8_t DLMS_HEADER_EXT_OFFSET = 2; // Extra offset for extended length header
|
||||
static constexpr uint8_t DLMS_CIPHER_OFFSET = 0;
|
||||
static constexpr uint8_t DLMS_SYST_OFFSET = 1;
|
||||
static constexpr uint8_t DLMS_LENGTH_OFFSET = 10;
|
||||
static constexpr uint8_t TWO_BYTE_LENGTH = 0x82;
|
||||
static constexpr uint8_t DLMS_LENGTH_CORRECTION = 5; // Header bytes included in length field
|
||||
static constexpr uint8_t DLMS_SECBYTE_OFFSET = 11;
|
||||
static constexpr uint8_t DLMS_FRAMECOUNTER_OFFSET = 12;
|
||||
static constexpr uint8_t DLMS_FRAMECOUNTER_LENGTH = 4;
|
||||
static constexpr uint8_t DLMS_PAYLOAD_OFFSET = 16;
|
||||
static constexpr uint8_t GLO_CIPHERING = 0xDB;
|
||||
static constexpr uint8_t DATA_NOTIFICATION = 0x0F;
|
||||
static constexpr uint8_t TIMESTAMP_DATETIME = 0x0C;
|
||||
static constexpr uint16_t MAX_MESSAGE_LENGTH = 512; // Maximum size of message (when having 2 bytes length in header).
|
||||
|
||||
// Provider specific quirks
|
||||
static constexpr uint8_t NETZ_NOE_MAGIC_BYTE = 0x81; // Magic length byte used by Netz NOE
|
||||
static constexpr uint8_t NETZ_NOE_EXPECTED_MESSAGE_LENGTH = 0xF8;
|
||||
static constexpr uint8_t NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE = 0x20;
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
@@ -1,468 +0,0 @@
|
||||
#include "dlms_meter.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
||||
#include <bearssl/bearssl.h>
|
||||
#elif defined(USE_ESP32)
|
||||
#include "mbedtls/esp_config.h"
|
||||
#include "mbedtls/gcm.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
static constexpr const char *TAG = "dlms_meter";
|
||||
|
||||
void DlmsMeterComponent::dump_config() {
|
||||
const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic";
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"DLMS Meter:\n"
|
||||
" Provider: %s\n"
|
||||
" Read Timeout: %u ms",
|
||||
provider_name, this->read_timeout_);
|
||||
#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_);
|
||||
DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, )
|
||||
#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_);
|
||||
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, )
|
||||
}
|
||||
|
||||
void DlmsMeterComponent::loop() {
|
||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||
while (this->available()) {
|
||||
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
|
||||
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
||||
break;
|
||||
}
|
||||
uint8_t c;
|
||||
this->read_byte(&c);
|
||||
this->receive_buffer_.push_back(c);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
|
||||
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
|
||||
this->mbus_payload_.clear();
|
||||
if (!this->parse_mbus_(this->mbus_payload_))
|
||||
return;
|
||||
|
||||
uint16_t message_length;
|
||||
uint8_t systitle_length;
|
||||
uint16_t header_offset;
|
||||
if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset))
|
||||
return;
|
||||
|
||||
if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) {
|
||||
ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt in place and then decode the OBIS codes
|
||||
if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset))
|
||||
return;
|
||||
this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length);
|
||||
}
|
||||
}
|
||||
|
||||
bool DlmsMeterComponent::parse_mbus_(std::vector<uint8_t> &mbus_payload) {
|
||||
ESP_LOGV(TAG, "Parsing M-Bus frames");
|
||||
uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames
|
||||
|
||||
while (frame_offset < this->receive_buffer_.size()) {
|
||||
// Ensure enough bytes remain for the minimal intro header before accessing indices
|
||||
if (this->receive_buffer_.size() - frame_offset < MBUS_HEADER_INTRO_LENGTH) {
|
||||
ESP_LOGE(TAG, "MBUS: Not enough data for frame header (need %d, have %d)", MBUS_HEADER_INTRO_LENGTH,
|
||||
(this->receive_buffer_.size() - frame_offset));
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check start bytes
|
||||
if (this->receive_buffer_[frame_offset + MBUS_START1_OFFSET] != START_BYTE_LONG_FRAME ||
|
||||
this->receive_buffer_[frame_offset + MBUS_START2_OFFSET] != START_BYTE_LONG_FRAME) {
|
||||
ESP_LOGE(TAG, "MBUS: Start bytes do not match");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Both length bytes must be identical
|
||||
if (this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET] !=
|
||||
this->receive_buffer_[frame_offset + MBUS_LENGTH2_OFFSET]) {
|
||||
ESP_LOGE(TAG, "MBUS: Length bytes do not match");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t frame_length = this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET]; // Get length of this frame
|
||||
|
||||
// Check if received data is enough for the given frame length
|
||||
if (this->receive_buffer_.size() - frame_offset <
|
||||
frame_length + 3) { // length field inside packet does not account for second start- + checksum- + stop- byte
|
||||
ESP_LOGE(TAG, "MBUS: Frame too big for received data");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we have full frame (header + payload + checksum + stop byte) before accessing stop byte
|
||||
size_t required_total =
|
||||
frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH; // payload + header + 2 footer bytes
|
||||
if (this->receive_buffer_.size() - frame_offset < required_total) {
|
||||
ESP_LOGE(TAG, "MBUS: Incomplete frame (need %d, have %d)", (unsigned int) required_total,
|
||||
this->receive_buffer_.size() - frame_offset);
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH - 1] !=
|
||||
STOP_BYTE) {
|
||||
ESP_LOGE(TAG, "MBUS: Invalid stop byte");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify checksum: sum of all bytes starting at MBUS_HEADER_INTRO_LENGTH, take last byte
|
||||
uint8_t checksum = 0; // use uint8_t so only the 8 least significant bits are stored
|
||||
for (uint16_t i = 0; i < frame_length; i++) {
|
||||
checksum += this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + i];
|
||||
}
|
||||
if (checksum != this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]) {
|
||||
ESP_LOGE(TAG, "MBUS: Invalid checksum: %x != %x", checksum,
|
||||
this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]);
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
mbus_payload.insert(mbus_payload.end(), &this->receive_buffer_[frame_offset + MBUS_FULL_HEADER_LENGTH],
|
||||
&this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + frame_length]);
|
||||
|
||||
frame_offset += MBUS_HEADER_INTRO_LENGTH + frame_length + MBUS_FOOTER_LENGTH;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DlmsMeterComponent::parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length,
|
||||
uint8_t &systitle_length, uint16_t &header_offset) {
|
||||
ESP_LOGV(TAG, "Parsing DLMS header");
|
||||
if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) {
|
||||
ESP_LOGE(TAG, "DLMS: Payload too short");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB)
|
||||
ESP_LOGE(TAG, "DLMS: Unsupported cipher");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
systitle_length = mbus_payload[DLMS_SYST_OFFSET];
|
||||
|
||||
if (systitle_length != 0x08) { // Only system titles with length of 8 are supported
|
||||
ESP_LOGE(TAG, "DLMS: Unsupported system title length");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
message_length = mbus_payload[DLMS_LENGTH_OFFSET];
|
||||
header_offset = 0;
|
||||
|
||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
||||
// for some reason EVN seems to set the standard "length" field to 0x81 and then the actual length is in the next
|
||||
// byte. Check some bytes to see if received data still matches expectation
|
||||
if (message_length == NETZ_NOE_MAGIC_BYTE &&
|
||||
mbus_payload[DLMS_LENGTH_OFFSET + 1] == NETZ_NOE_EXPECTED_MESSAGE_LENGTH &&
|
||||
mbus_payload[DLMS_LENGTH_OFFSET + 2] == NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE) {
|
||||
message_length = mbus_payload[DLMS_LENGTH_OFFSET + 1];
|
||||
header_offset = 1;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Wrong Length - Security Control Byte sequence detected for provider EVN");
|
||||
}
|
||||
} else {
|
||||
if (message_length == TWO_BYTE_LENGTH) {
|
||||
message_length = encode_uint16(mbus_payload[DLMS_LENGTH_OFFSET + 1], mbus_payload[DLMS_LENGTH_OFFSET + 2]);
|
||||
header_offset = DLMS_HEADER_EXT_OFFSET;
|
||||
}
|
||||
}
|
||||
if (message_length < DLMS_LENGTH_CORRECTION) {
|
||||
ESP_LOGE(TAG, "DLMS: Message length too short: %u", message_length);
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
message_length -= DLMS_LENGTH_CORRECTION; // Correct message length due to part of header being included in length
|
||||
|
||||
if (mbus_payload.size() - DLMS_HEADER_LENGTH - header_offset != message_length) {
|
||||
ESP_LOGV(TAG, "DLMS: Length mismatch - payload=%d, header=%d, offset=%d, message=%d", mbus_payload.size(),
|
||||
DLMS_HEADER_LENGTH, header_offset, message_length);
|
||||
ESP_LOGE(TAG, "DLMS: Message has invalid length");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != 0x21 &&
|
||||
mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] !=
|
||||
0x20) { // Only certain security suite is supported (0x21 || 0x20)
|
||||
ESP_LOGE(TAG, "DLMS: Unsupported security control byte");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
|
||||
uint16_t header_offset) {
|
||||
ESP_LOGV(TAG, "Decrypting payload");
|
||||
uint8_t iv[12]; // Reserve space for the IV, always 12 bytes
|
||||
// Copy system title to IV (System title is before length; no header offset needed!)
|
||||
// Add 1 to the offset in order to skip the system title length byte
|
||||
memcpy(&iv[0], &mbus_payload[DLMS_SYST_OFFSET + 1], systitle_length);
|
||||
memcpy(&iv[8], &mbus_payload[header_offset + DLMS_FRAMECOUNTER_OFFSET],
|
||||
DLMS_FRAMECOUNTER_LENGTH); // Copy frame counter to IV
|
||||
|
||||
uint8_t *payload_ptr = &mbus_payload[header_offset + DLMS_PAYLOAD_OFFSET];
|
||||
|
||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
||||
br_gcm_context gcm_ctx;
|
||||
br_aes_ct_ctr_keys bc;
|
||||
br_aes_ct_ctr_init(&bc, this->decryption_key_.data(), this->decryption_key_.size());
|
||||
br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32);
|
||||
br_gcm_reset(&gcm_ctx, iv, sizeof(iv));
|
||||
br_gcm_flip(&gcm_ctx);
|
||||
br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
|
||||
#elif defined(USE_ESP32)
|
||||
size_t outlen = 0;
|
||||
mbedtls_gcm_context gcm_ctx;
|
||||
mbedtls_gcm_init(&gcm_ctx);
|
||||
mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8);
|
||||
mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv));
|
||||
auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen);
|
||||
mbedtls_gcm_free(&gcm_ctx);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "Decryption failed with error: %d", ret);
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
#error "Invalid Platform"
|
||||
#endif
|
||||
|
||||
if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) {
|
||||
ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_length) {
|
||||
ESP_LOGV(TAG, "Decoding payload");
|
||||
MeterData data{};
|
||||
uint16_t current_position = DECODER_START_OFFSET;
|
||||
bool power_factor_found = false;
|
||||
|
||||
while (current_position + OBIS_CODE_OFFSET <= message_length) {
|
||||
if (plaintext[current_position + OBIS_TYPE_OFFSET] != DataType::OCTET_STRING) {
|
||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header type: %x", plaintext[current_position + OBIS_TYPE_OFFSET]);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t obis_code_length = plaintext[current_position + OBIS_LENGTH_OFFSET];
|
||||
if (obis_code_length != OBIS_CODE_LENGTH_STANDARD && obis_code_length != OBIS_CODE_LENGTH_EXTENDED) {
|
||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header length: %x", obis_code_length);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
if (current_position + OBIS_CODE_OFFSET + obis_code_length > message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for OBIS code");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *obis_code = &plaintext[current_position + OBIS_CODE_OFFSET];
|
||||
uint8_t obis_medium = obis_code[OBIS_A];
|
||||
uint16_t obis_cd = encode_uint16(obis_code[OBIS_C], obis_code[OBIS_D]);
|
||||
|
||||
bool timestamp_found = false;
|
||||
bool meter_number_found = false;
|
||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
||||
// Do not advance Position when reading the Timestamp at DECODER_START_OFFSET
|
||||
if ((obis_code_length == OBIS_CODE_LENGTH_EXTENDED) && (current_position == DECODER_START_OFFSET)) {
|
||||
timestamp_found = true;
|
||||
} else if (power_factor_found) {
|
||||
meter_number_found = true;
|
||||
power_factor_found = false;
|
||||
} else {
|
||||
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code and position
|
||||
}
|
||||
} else {
|
||||
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code, position and type
|
||||
}
|
||||
if (!timestamp_found && !meter_number_found && obis_medium != Medium::ELECTRICITY &&
|
||||
obis_medium != Medium::ABSTRACT) {
|
||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS medium: %x", obis_medium);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (current_position >= message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for data type");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
float value = 0.0f;
|
||||
uint8_t value_size = 0;
|
||||
uint8_t data_type = plaintext[current_position];
|
||||
current_position++;
|
||||
|
||||
switch (data_type) {
|
||||
case DataType::DOUBLE_LONG_UNSIGNED: {
|
||||
value_size = 4;
|
||||
if (current_position + value_size > message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for DOUBLE_LONG_UNSIGNED");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
value = encode_uint32(plaintext[current_position + 0], plaintext[current_position + 1],
|
||||
plaintext[current_position + 2], plaintext[current_position + 3]);
|
||||
current_position += value_size;
|
||||
break;
|
||||
}
|
||||
case DataType::LONG_UNSIGNED: {
|
||||
value_size = 2;
|
||||
if (current_position + value_size > message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for LONG_UNSIGNED");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
value = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
|
||||
current_position += value_size;
|
||||
break;
|
||||
}
|
||||
case DataType::OCTET_STRING: {
|
||||
uint8_t data_length = plaintext[current_position];
|
||||
current_position++; // Advance past string length
|
||||
if (current_position + data_length > message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for OCTET_STRING");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
// Handle timestamp (normal OBIS code or NETZNOE special case)
|
||||
if (obis_cd == OBIS_TIMESTAMP || timestamp_found) {
|
||||
if (data_length < 8) {
|
||||
ESP_LOGE(TAG, "OBIS: Timestamp data too short: %u", data_length);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
uint16_t year = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
|
||||
uint8_t month = plaintext[current_position + 2];
|
||||
uint8_t day = plaintext[current_position + 3];
|
||||
uint8_t hour = plaintext[current_position + 5];
|
||||
uint8_t minute = plaintext[current_position + 6];
|
||||
uint8_t second = plaintext[current_position + 7];
|
||||
if (year > 9999 || month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59) {
|
||||
ESP_LOGE(TAG, "Invalid timestamp values: %04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, minute,
|
||||
second);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
snprintf(data.timestamp, sizeof(data.timestamp), "%04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour,
|
||||
minute, second);
|
||||
} else if (meter_number_found) {
|
||||
snprintf(data.meternumber, sizeof(data.meternumber), "%.*s", data_length, &plaintext[current_position]);
|
||||
}
|
||||
current_position += data_length;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS data type: %x", data_type);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip break after data
|
||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
||||
// Don't skip the break on the first timestamp, as there's none
|
||||
if (!timestamp_found) {
|
||||
current_position += 2;
|
||||
}
|
||||
} else {
|
||||
current_position += 2;
|
||||
}
|
||||
|
||||
// Check for additional data (scaler-unit structure)
|
||||
if (current_position < message_length && plaintext[current_position] == DataType::INTEGER) {
|
||||
// Apply scaler: real_value = raw_value × 10^scaler
|
||||
if (current_position + 1 < message_length) {
|
||||
int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
|
||||
if (scaler != 0) {
|
||||
value *= powf(10.0f, scaler);
|
||||
}
|
||||
}
|
||||
|
||||
// on EVN Meters there is no additional break
|
||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
||||
current_position += 4;
|
||||
} else {
|
||||
current_position += 6;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED)
|
||||
if (value_size > 0) {
|
||||
switch (obis_cd) {
|
||||
case OBIS_VOLTAGE_L1:
|
||||
data.voltage_l1 = value;
|
||||
break;
|
||||
case OBIS_VOLTAGE_L2:
|
||||
data.voltage_l2 = value;
|
||||
break;
|
||||
case OBIS_VOLTAGE_L3:
|
||||
data.voltage_l3 = value;
|
||||
break;
|
||||
case OBIS_CURRENT_L1:
|
||||
data.current_l1 = value;
|
||||
break;
|
||||
case OBIS_CURRENT_L2:
|
||||
data.current_l2 = value;
|
||||
break;
|
||||
case OBIS_CURRENT_L3:
|
||||
data.current_l3 = value;
|
||||
break;
|
||||
case OBIS_ACTIVE_POWER_PLUS:
|
||||
data.active_power_plus = value;
|
||||
break;
|
||||
case OBIS_ACTIVE_POWER_MINUS:
|
||||
data.active_power_minus = value;
|
||||
break;
|
||||
case OBIS_ACTIVE_ENERGY_PLUS:
|
||||
data.active_energy_plus = value;
|
||||
break;
|
||||
case OBIS_ACTIVE_ENERGY_MINUS:
|
||||
data.active_energy_minus = value;
|
||||
break;
|
||||
case OBIS_REACTIVE_ENERGY_PLUS:
|
||||
data.reactive_energy_plus = value;
|
||||
break;
|
||||
case OBIS_REACTIVE_ENERGY_MINUS:
|
||||
data.reactive_energy_minus = value;
|
||||
break;
|
||||
case OBIS_POWER_FACTOR:
|
||||
data.power_factor = value;
|
||||
power_factor_found = true;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->receive_buffer_.clear();
|
||||
|
||||
ESP_LOGI(TAG, "Received valid data");
|
||||
this->publish_sensors(data);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
@@ -1,96 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include "mbus.h"
|
||||
#include "dlms.h"
|
||||
#include "obis.h"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
#ifndef DLMS_METER_SENSOR_LIST
|
||||
#define DLMS_METER_SENSOR_LIST(F, SEP)
|
||||
#endif
|
||||
|
||||
#ifndef DLMS_METER_TEXT_SENSOR_LIST
|
||||
#define DLMS_METER_TEXT_SENSOR_LIST(F, SEP)
|
||||
#endif
|
||||
|
||||
struct MeterData {
|
||||
float voltage_l1 = 0.0f; // Voltage L1
|
||||
float voltage_l2 = 0.0f; // Voltage L2
|
||||
float voltage_l3 = 0.0f; // Voltage L3
|
||||
float current_l1 = 0.0f; // Current L1
|
||||
float current_l2 = 0.0f; // Current L2
|
||||
float current_l3 = 0.0f; // Current L3
|
||||
float active_power_plus = 0.0f; // Active power taken from grid
|
||||
float active_power_minus = 0.0f; // Active power put into grid
|
||||
float active_energy_plus = 0.0f; // Active energy taken from grid
|
||||
float active_energy_minus = 0.0f; // Active energy put into grid
|
||||
float reactive_energy_plus = 0.0f; // Reactive energy taken from grid
|
||||
float reactive_energy_minus = 0.0f; // Reactive energy put into grid
|
||||
char timestamp[27]{}; // Text sensor for the timestamp value
|
||||
|
||||
// Netz NOE
|
||||
float power_factor = 0.0f; // Power Factor
|
||||
char meternumber[13]{}; // Text sensor for the meterNumber value
|
||||
};
|
||||
|
||||
// Provider constants
|
||||
enum Providers : uint32_t { PROVIDER_GENERIC = 0x00, PROVIDER_NETZNOE = 0x01 };
|
||||
|
||||
class DlmsMeterComponent : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
DlmsMeterComponent() = default;
|
||||
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_decryption_key(const std::array<uint8_t, 16> &key) { this->decryption_key_ = key; }
|
||||
void set_provider(uint32_t provider) { this->provider_ = provider; }
|
||||
|
||||
void publish_sensors(MeterData &data) {
|
||||
#define DLMS_METER_PUBLISH_SENSOR(s) \
|
||||
if (this->s##_sensor_ != nullptr) \
|
||||
s##_sensor_->publish_state(data.s);
|
||||
DLMS_METER_SENSOR_LIST(DLMS_METER_PUBLISH_SENSOR, )
|
||||
|
||||
#define DLMS_METER_PUBLISH_TEXT_SENSOR(s) \
|
||||
if (this->s##_text_sensor_ != nullptr) \
|
||||
s##_text_sensor_->publish_state(data.s);
|
||||
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_PUBLISH_TEXT_SENSOR, )
|
||||
}
|
||||
|
||||
DLMS_METER_SENSOR_LIST(SUB_SENSOR, )
|
||||
DLMS_METER_TEXT_SENSOR_LIST(SUB_TEXT_SENSOR, )
|
||||
|
||||
protected:
|
||||
bool parse_mbus_(std::vector<uint8_t> &mbus_payload);
|
||||
bool parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length, uint8_t &systitle_length,
|
||||
uint16_t &header_offset);
|
||||
bool decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
|
||||
uint16_t header_offset);
|
||||
void decode_obis_(uint8_t *plaintext, uint16_t message_length);
|
||||
|
||||
std::vector<uint8_t> receive_buffer_; // Stores the packet currently being received
|
||||
std::vector<uint8_t> mbus_payload_; // Parsed M-Bus payload, reused to avoid heap churn
|
||||
uint32_t last_read_ = 0; // Timestamp when data was last read
|
||||
uint32_t read_timeout_ = 1000; // Time to wait after last byte before considering data complete
|
||||
|
||||
uint32_t provider_ = PROVIDER_GENERIC; // Provider of the meter / your grid operator
|
||||
std::array<uint8_t, 16> decryption_key_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
@@ -1,69 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
/*
|
||||
+----------------------------------------------------+ -
|
||||
| Start Character [0x68] | \
|
||||
+----------------------------------------------------+ |
|
||||
| Data Length (L) | |
|
||||
+----------------------------------------------------+ |
|
||||
| Data Length Repeat (L) | |
|
||||
+----------------------------------------------------+ > M-Bus Data link layer
|
||||
| Start Character Repeat [0x68] | |
|
||||
+----------------------------------------------------+ |
|
||||
| Control/Function Field (C) | |
|
||||
+----------------------------------------------------+ |
|
||||
| Address Field (A) | /
|
||||
+----------------------------------------------------+ -
|
||||
| Control Information Field (CI) | \
|
||||
+----------------------------------------------------+ |
|
||||
| Source Transport Service Access Point (STSAP) | > DLMS/COSEM M-Bus transport layer
|
||||
+----------------------------------------------------+ |
|
||||
| Destination Transport Service Access Point (DTSAP) | /
|
||||
+----------------------------------------------------+ -
|
||||
| | \
|
||||
~ ~ |
|
||||
Data > DLMS/COSEM Application Layer
|
||||
~ ~ |
|
||||
| | /
|
||||
+----------------------------------------------------+ -
|
||||
| Checksum | \
|
||||
+----------------------------------------------------+ > M-Bus Data link layer
|
||||
| Stop Character [0x16] | /
|
||||
+----------------------------------------------------+ -
|
||||
|
||||
Data_Length = L - C - A - CI
|
||||
Each line (except Data) is one Byte
|
||||
|
||||
Possible Values found in publicly available docs:
|
||||
- C: 0x53/0x73 (SND_UD)
|
||||
- A: FF (Broadcast)
|
||||
- CI: 0x00-0x1F/0x60/0x61/0x7C/0x7D
|
||||
- STSAP: 0x01 (Management Logical Device ID 1 of the meter)
|
||||
- DTSAP: 0x67 (Consumer Information Push Client ID 103)
|
||||
*/
|
||||
|
||||
// MBUS start bytes for different telegram formats:
|
||||
// - Single Character: 0xE5 (length=1)
|
||||
// - Short Frame: 0x10 (length=5)
|
||||
// - Control Frame: 0x68 (length=9)
|
||||
// - Long Frame: 0x68 (length=9+data_length)
|
||||
// This component currently only uses Long Frame.
|
||||
static constexpr uint8_t START_BYTE_SINGLE_CHARACTER = 0xE5;
|
||||
static constexpr uint8_t START_BYTE_SHORT_FRAME = 0x10;
|
||||
static constexpr uint8_t START_BYTE_CONTROL_FRAME = 0x68;
|
||||
static constexpr uint8_t START_BYTE_LONG_FRAME = 0x68;
|
||||
static constexpr uint8_t MBUS_HEADER_INTRO_LENGTH = 4; // Header length for the intro (0x68, length, length, 0x68)
|
||||
static constexpr uint8_t MBUS_FULL_HEADER_LENGTH = 9; // Total header length
|
||||
static constexpr uint8_t MBUS_FOOTER_LENGTH = 2; // Footer after frame
|
||||
static constexpr uint8_t MBUS_MAX_FRAME_LENGTH = 250; // Maximum size of frame
|
||||
static constexpr uint8_t MBUS_START1_OFFSET = 0; // Offset of first start byte
|
||||
static constexpr uint8_t MBUS_LENGTH1_OFFSET = 1; // Offset of first length byte
|
||||
static constexpr uint8_t MBUS_LENGTH2_OFFSET = 2; // Offset of (duplicated) second length byte
|
||||
static constexpr uint8_t MBUS_START2_OFFSET = 3; // Offset of (duplicated) second start byte
|
||||
static constexpr uint8_t STOP_BYTE = 0x16;
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
@@ -1,94 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
// Data types as per specification
|
||||
enum DataType {
|
||||
NULL_DATA = 0x00,
|
||||
BOOLEAN = 0x03,
|
||||
BIT_STRING = 0x04,
|
||||
DOUBLE_LONG = 0x05,
|
||||
DOUBLE_LONG_UNSIGNED = 0x06,
|
||||
OCTET_STRING = 0x09,
|
||||
VISIBLE_STRING = 0x0A,
|
||||
UTF8_STRING = 0x0C,
|
||||
BINARY_CODED_DECIMAL = 0x0D,
|
||||
INTEGER = 0x0F,
|
||||
LONG = 0x10,
|
||||
UNSIGNED = 0x11,
|
||||
LONG_UNSIGNED = 0x12,
|
||||
LONG64 = 0x14,
|
||||
LONG64_UNSIGNED = 0x15,
|
||||
ENUM = 0x16,
|
||||
FLOAT32 = 0x17,
|
||||
FLOAT64 = 0x18,
|
||||
DATE_TIME = 0x19,
|
||||
DATE = 0x1A,
|
||||
TIME = 0x1B,
|
||||
|
||||
ARRAY = 0x01,
|
||||
STRUCTURE = 0x02,
|
||||
COMPACT_ARRAY = 0x13
|
||||
};
|
||||
|
||||
enum Medium {
|
||||
ABSTRACT = 0x00,
|
||||
ELECTRICITY = 0x01,
|
||||
HEAT_COST_ALLOCATOR = 0x04,
|
||||
COOLING = 0x05,
|
||||
HEAT = 0x06,
|
||||
GAS = 0x07,
|
||||
COLD_WATER = 0x08,
|
||||
HOT_WATER = 0x09,
|
||||
OIL = 0x10,
|
||||
COMPRESSED_AIR = 0x11,
|
||||
NITROGEN = 0x12
|
||||
};
|
||||
|
||||
// Data structure
|
||||
static constexpr uint8_t DECODER_START_OFFSET = 20; // Skip header, timestamp and break block
|
||||
static constexpr uint8_t OBIS_TYPE_OFFSET = 0;
|
||||
static constexpr uint8_t OBIS_LENGTH_OFFSET = 1;
|
||||
static constexpr uint8_t OBIS_CODE_OFFSET = 2;
|
||||
static constexpr uint8_t OBIS_CODE_LENGTH_STANDARD = 0x06; // 6-byte OBIS code (A.B.C.D.E.F)
|
||||
static constexpr uint8_t OBIS_CODE_LENGTH_EXTENDED = 0x0C; // 12-byte extended OBIS code
|
||||
static constexpr uint8_t OBIS_A = 0;
|
||||
static constexpr uint8_t OBIS_B = 1;
|
||||
static constexpr uint8_t OBIS_C = 2;
|
||||
static constexpr uint8_t OBIS_D = 3;
|
||||
static constexpr uint8_t OBIS_E = 4;
|
||||
static constexpr uint8_t OBIS_F = 5;
|
||||
|
||||
// Metadata
|
||||
static constexpr uint16_t OBIS_TIMESTAMP = 0x0100;
|
||||
static constexpr uint16_t OBIS_SERIAL_NUMBER = 0x6001;
|
||||
static constexpr uint16_t OBIS_DEVICE_NAME = 0x2A00;
|
||||
|
||||
// Voltage
|
||||
static constexpr uint16_t OBIS_VOLTAGE_L1 = 0x2007;
|
||||
static constexpr uint16_t OBIS_VOLTAGE_L2 = 0x3407;
|
||||
static constexpr uint16_t OBIS_VOLTAGE_L3 = 0x4807;
|
||||
|
||||
// Current
|
||||
static constexpr uint16_t OBIS_CURRENT_L1 = 0x1F07;
|
||||
static constexpr uint16_t OBIS_CURRENT_L2 = 0x3307;
|
||||
static constexpr uint16_t OBIS_CURRENT_L3 = 0x4707;
|
||||
|
||||
// Power
|
||||
static constexpr uint16_t OBIS_ACTIVE_POWER_PLUS = 0x0107;
|
||||
static constexpr uint16_t OBIS_ACTIVE_POWER_MINUS = 0x0207;
|
||||
|
||||
// Active energy
|
||||
static constexpr uint16_t OBIS_ACTIVE_ENERGY_PLUS = 0x0108;
|
||||
static constexpr uint16_t OBIS_ACTIVE_ENERGY_MINUS = 0x0208;
|
||||
|
||||
// Reactive energy
|
||||
static constexpr uint16_t OBIS_REACTIVE_ENERGY_PLUS = 0x0308;
|
||||
static constexpr uint16_t OBIS_REACTIVE_ENERGY_MINUS = 0x0408;
|
||||
|
||||
// Netz NOE specific
|
||||
static constexpr uint16_t OBIS_POWER_FACTOR = 0x0D07;
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
@@ -1,124 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
|
||||
|
||||
AUTO_LOAD = ["dlms_meter"]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Optional("voltage_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("voltage_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("voltage_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_power_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_power_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_energy_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("active_energy_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
# Netz NOE
|
||||
cv.Optional("power_factor"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_POWER_FACTOR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
|
||||
sensors = []
|
||||
for key, conf in config.items():
|
||||
if not isinstance(conf, dict):
|
||||
continue
|
||||
id = conf[CONF_ID]
|
||||
if id and id.type == sensor.Sensor:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
|
||||
sensors.append(f"F({key})")
|
||||
|
||||
if sensors:
|
||||
cg.add_define(
|
||||
"DLMS_METER_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
|
||||
)
|
||||
@@ -1,37 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
|
||||
|
||||
AUTO_LOAD = ["dlms_meter"]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
|
||||
# Netz NOE
|
||||
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
|
||||
text_sensors = []
|
||||
for key, conf in config.items():
|
||||
if not isinstance(conf, dict):
|
||||
continue
|
||||
id = conf[CONF_ID]
|
||||
if id and id.type == text_sensor.TextSensor:
|
||||
sens = await text_sensor.new_text_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
|
||||
text_sensors.append(f"F({key})")
|
||||
|
||||
if text_sensors:
|
||||
cg.add_define(
|
||||
"DLMS_METER_TEXT_SENSOR_LIST(F, sep)",
|
||||
cg.RawExpression(" sep ".join(text_sensors)),
|
||||
)
|
||||
@@ -141,15 +141,15 @@ void EndstopCover::start_direction_(CoverOperation dir) {
|
||||
Trigger<> *trig;
|
||||
switch (dir) {
|
||||
case COVER_OPERATION_IDLE:
|
||||
trig = &this->stop_trigger_;
|
||||
trig = this->stop_trigger_;
|
||||
break;
|
||||
case COVER_OPERATION_OPENING:
|
||||
this->last_operation_ = dir;
|
||||
trig = &this->open_trigger_;
|
||||
trig = this->open_trigger_;
|
||||
break;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
this->last_operation_ = dir;
|
||||
trig = &this->close_trigger_;
|
||||
trig = this->close_trigger_;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
||||
@@ -15,9 +15,9 @@ class EndstopCover : public cover::Cover, public Component {
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
Trigger<> *get_open_trigger() { return &this->open_trigger_; }
|
||||
Trigger<> *get_close_trigger() { return &this->close_trigger_; }
|
||||
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
|
||||
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
|
||||
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
|
||||
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
|
||||
void set_open_endstop(binary_sensor::BinarySensor *open_endstop) { this->open_endstop_ = open_endstop; }
|
||||
void set_close_endstop(binary_sensor::BinarySensor *close_endstop) { this->close_endstop_ = close_endstop; }
|
||||
void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
|
||||
@@ -39,11 +39,11 @@ class EndstopCover : public cover::Cover, public Component {
|
||||
|
||||
binary_sensor::BinarySensor *open_endstop_;
|
||||
binary_sensor::BinarySensor *close_endstop_;
|
||||
Trigger<> open_trigger_;
|
||||
Trigger<> *open_trigger_{new Trigger<>()};
|
||||
uint32_t open_duration_;
|
||||
Trigger<> close_trigger_;
|
||||
Trigger<> *close_trigger_{new Trigger<>()};
|
||||
uint32_t close_duration_;
|
||||
Trigger<> stop_trigger_;
|
||||
Trigger<> *stop_trigger_{new Trigger<>()};
|
||||
uint32_t max_duration_{UINT32_MAX};
|
||||
|
||||
Trigger<> *prev_command_trigger_{nullptr};
|
||||
|
||||
@@ -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}"
|
||||
@@ -935,10 +739,9 @@ CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||
|
||||
# VFS requirement tracking
|
||||
# Components that need VFS features can call require_vfs_*() functions
|
||||
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
|
||||
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
|
||||
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
|
||||
KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
|
||||
# Feature requirement tracking - components can call require_* functions to re-enable
|
||||
# These are stored in CORE.data[KEY_ESP32] dict
|
||||
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
|
||||
@@ -965,15 +768,6 @@ def require_vfs_dir() -> None:
|
||||
CORE.data[KEY_VFS_DIR_REQUIRED] = True
|
||||
|
||||
|
||||
def require_vfs_termios() -> None:
|
||||
"""Mark that VFS termios support is required by a component.
|
||||
|
||||
Call this from components that use terminal I/O functions (usb_serial_jtag_vfs_*, etc.).
|
||||
This prevents CONFIG_VFS_SUPPORT_TERMIOS from being disabled.
|
||||
"""
|
||||
CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True
|
||||
|
||||
|
||||
def require_full_certificate_bundle() -> None:
|
||||
"""Request the full certificate bundle instead of the common-CAs-only bundle.
|
||||
|
||||
@@ -1322,27 +1116,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."""
|
||||
@@ -1546,10 +1319,6 @@ async def to_code(config):
|
||||
# Disable dynamic log level control to save memory
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
|
||||
|
||||
# Disable per-tag log level filtering since dynamic level control is disabled above
|
||||
# This saves ~250 bytes of RAM (tag cache) and associated code
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True)
|
||||
|
||||
# Reduce PHY TX power in the event of a brownout
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
|
||||
|
||||
@@ -1603,18 +1372,11 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
|
||||
|
||||
# Disable VFS support for termios (terminal I/O functions)
|
||||
# USB Serial JTAG VFS functions require termios support.
|
||||
# Components that need it (e.g., logger when USB_SERIAL_JTAG is supported but not selected
|
||||
# as the logger output) call require_vfs_termios().
|
||||
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
|
||||
# Saves approximately 1.8KB of flash when disabled (default).
|
||||
if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False):
|
||||
# Component requires VFS termios - force enable regardless of user setting
|
||||
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_TERMIOS", True)
|
||||
else:
|
||||
# No component needs it - allow user to control (default: disabled)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
|
||||
)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
|
||||
)
|
||||
|
||||
# Disable VFS support for select() with file descriptors
|
||||
# ESPHome only uses select() with sockets via lwip_select(), which still works.
|
||||
@@ -1761,11 +1523,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 +1603,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 +1615,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 {
|
||||
@@ -194,11 +203,10 @@ class ESP32Preferences : public ESPPreferences {
|
||||
}
|
||||
};
|
||||
|
||||
static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void setup_preferences() {
|
||||
s_preferences.open();
|
||||
global_preferences = &s_preferences;
|
||||
auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
prefs->open();
|
||||
global_preferences = prefs;
|
||||
}
|
||||
|
||||
} // namespace esp32
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,6 +17,10 @@ namespace esphome::esp8266 {
|
||||
|
||||
static const char *const TAG = "esp8266.preferences";
|
||||
|
||||
static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
|
||||
@@ -39,11 +43,6 @@ static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
|
||||
#endif
|
||||
|
||||
static uint32_t
|
||||
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
|
||||
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
|
||||
return false;
|
||||
@@ -181,6 +180,7 @@ class ESP8266Preferences : public ESPPreferences {
|
||||
uint32_t current_flash_offset = 0; // in words
|
||||
|
||||
void setup() {
|
||||
s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT
|
||||
ESP_LOGVV(TAG, "Loading preferences from flash");
|
||||
|
||||
{
|
||||
@@ -283,11 +283,10 @@ class ESP8266Preferences : public ESPPreferences {
|
||||
}
|
||||
};
|
||||
|
||||
static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void setup_preferences() {
|
||||
s_preferences.setup();
|
||||
global_preferences = &s_preferences;
|
||||
auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->setup();
|
||||
global_preferences = pref;
|
||||
}
|
||||
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation, pins
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
@@ -35,8 +35,6 @@ from esphome.const import (
|
||||
CONF_MODE,
|
||||
CONF_MOSI_PIN,
|
||||
CONF_NUMBER,
|
||||
CONF_ON_CONNECT,
|
||||
CONF_ON_DISCONNECT,
|
||||
CONF_PAGE_ID,
|
||||
CONF_PIN,
|
||||
CONF_POLLING_INTERVAL,
|
||||
@@ -239,8 +237,6 @@ BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -431,29 +427,8 @@ async def to_code(config):
|
||||
# Add LAN867x 10BASE-T1S PHY support component
|
||||
add_idf_component(name="espressif/lan867x", ref="2.0.0")
|
||||
|
||||
if on_connect_config := config.get(CONF_ON_CONNECT):
|
||||
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_connect_trigger(), [], on_connect_config
|
||||
)
|
||||
|
||||
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
|
||||
cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_disconnect_trigger(), [], on_disconnect_config
|
||||
)
|
||||
|
||||
if on_connect_config := config.get(CONF_ON_CONNECT):
|
||||
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_connect_trigger(), [], on_connect_config
|
||||
)
|
||||
|
||||
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
|
||||
cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_disconnect_trigger(), [], on_disconnect_config
|
||||
)
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
CORE.add_job(final_step)
|
||||
|
||||
|
||||
@@ -309,9 +309,6 @@ void EthernetComponent::loop() {
|
||||
|
||||
this->dump_connect_params_();
|
||||
this->status_clear_warning();
|
||||
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
||||
this->connect_trigger_.trigger();
|
||||
#endif
|
||||
} else if (now - this->connect_begin_ > 15000) {
|
||||
ESP_LOGW(TAG, "Connecting failed; reconnecting");
|
||||
this->start_connect_();
|
||||
@@ -321,16 +318,10 @@ void EthernetComponent::loop() {
|
||||
if (!this->started_) {
|
||||
ESP_LOGI(TAG, "Stopped connection");
|
||||
this->state_ = EthernetComponentState::STOPPED;
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
this->disconnect_trigger_.trigger();
|
||||
#endif
|
||||
} else if (!this->connected_) {
|
||||
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
||||
this->state_ = EthernetComponentState::CONNECTING;
|
||||
this->start_connect_();
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
this->disconnect_trigger_.trigger();
|
||||
#endif
|
||||
} else {
|
||||
this->finish_connect_();
|
||||
// When connected and stable, disable the loop to save CPU cycles
|
||||
@@ -811,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`.
|
||||
@@ -829,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
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -120,12 +119,6 @@ class EthernetComponent : public Component {
|
||||
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
||||
Trigger<> *get_connect_trigger() { return &this->connect_trigger_; }
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; }
|
||||
#endif
|
||||
protected:
|
||||
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
|
||||
static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
|
||||
@@ -197,12 +190,6 @@ class EthernetComponent : public Component {
|
||||
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
||||
Trigger<> connect_trigger_;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
Trigger<> disconnect_trigger_;
|
||||
#endif
|
||||
private:
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -335,18 +335,18 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
|
||||
|
||||
switch (dir) {
|
||||
case COVER_OPERATION_IDLE:
|
||||
trig = &this->stop_trigger_;
|
||||
trig = this->stop_trigger_;
|
||||
break;
|
||||
case COVER_OPERATION_OPENING:
|
||||
this->last_operation_ = dir;
|
||||
trig = &this->open_trigger_;
|
||||
trig = this->open_trigger_;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
obstacle = this->open_obstacle_;
|
||||
#endif
|
||||
break;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
this->last_operation_ = dir;
|
||||
trig = &this->close_trigger_;
|
||||
trig = this->close_trigger_;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
obstacle = this->close_obstacle_;
|
||||
#endif
|
||||
|
||||
@@ -17,9 +17,9 @@ class FeedbackCover : public cover::Cover, public Component {
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
Trigger<> *get_open_trigger() { return &this->open_trigger_; }
|
||||
Trigger<> *get_close_trigger() { return &this->close_trigger_; }
|
||||
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
|
||||
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
|
||||
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
|
||||
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void set_open_endstop(binary_sensor::BinarySensor *open_endstop);
|
||||
@@ -61,9 +61,9 @@ class FeedbackCover : public cover::Cover, public Component {
|
||||
binary_sensor::BinarySensor *close_obstacle_{nullptr};
|
||||
|
||||
#endif
|
||||
Trigger<> open_trigger_;
|
||||
Trigger<> close_trigger_;
|
||||
Trigger<> stop_trigger_;
|
||||
Trigger<> *open_trigger_{new Trigger<>()};
|
||||
Trigger<> *close_trigger_{new Trigger<>()};
|
||||
Trigger<> *stop_trigger_{new Trigger<>()};
|
||||
|
||||
uint32_t open_duration_{0};
|
||||
uint32_t close_duration_{0};
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -66,11 +66,10 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ
|
||||
return ESPPreferenceObject(backend);
|
||||
};
|
||||
|
||||
static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void setup_preferences() {
|
||||
host_preferences = &s_preferences;
|
||||
global_preferences = &s_preferences;
|
||||
auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
host_preferences = pref;
|
||||
global_preferences = pref;
|
||||
}
|
||||
|
||||
bool HostPreferenceBackend::save(const uint8_t *data, size_t len) {
|
||||
|
||||
@@ -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)
|
||||
@@ -356,13 +332,13 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; }
|
||||
|
||||
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() {
|
||||
return &this->success_trigger_with_response_;
|
||||
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() const {
|
||||
return this->success_trigger_with_response_;
|
||||
}
|
||||
#endif
|
||||
Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() { return &this->success_trigger_; }
|
||||
Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() const { return this->success_trigger_; }
|
||||
|
||||
Trigger<Ts...> *get_error_trigger() { return &this->error_trigger_; }
|
||||
Trigger<Ts...> *get_error_trigger() const { return this->error_trigger_; }
|
||||
|
||||
void set_max_response_buffer_size(size_t max_response_buffer_size) {
|
||||
this->max_response_buffer_size_ = max_response_buffer_size;
|
||||
@@ -396,7 +372,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
auto captured_args = std::make_tuple(x...);
|
||||
|
||||
if (container == nullptr) {
|
||||
std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); },
|
||||
std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); },
|
||||
captured_args);
|
||||
return;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -431,14 +406,14 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
}
|
||||
std::apply(
|
||||
[this, &container, &response_body](Ts... captured_args_inner) {
|
||||
this->success_trigger_with_response_.trigger(container, response_body, captured_args_inner...);
|
||||
this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...);
|
||||
},
|
||||
captured_args);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
std::apply([this, &container](
|
||||
Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); },
|
||||
Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); },
|
||||
captured_args);
|
||||
}
|
||||
container->end();
|
||||
@@ -458,10 +433,12 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
|
||||
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
|
||||
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> success_trigger_with_response_;
|
||||
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *success_trigger_with_response_ =
|
||||
new Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...>();
|
||||
#endif
|
||||
Trigger<std::shared_ptr<HttpContainer>, Ts...> success_trigger_;
|
||||
Trigger<Ts...> error_trigger_;
|
||||
Trigger<std::shared_ptr<HttpContainer>, Ts...> *success_trigger_ =
|
||||
new Trigger<std::shared_ptr<HttpContainer>, Ts...>();
|
||||
Trigger<Ts...> *error_trigger_ = new Trigger<Ts...>();
|
||||
|
||||
size_t max_response_buffer_size_{SIZE_MAX};
|
||||
};
|
||||
|
||||
@@ -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() ||
|
||||
|
||||
@@ -11,6 +11,12 @@ namespace i2c {
|
||||
static const char *const TAG = "i2c";
|
||||
|
||||
void I2CBus::i2c_scan_() {
|
||||
// suppress logs from the IDF I2C library during the scan
|
||||
#if defined(USE_ESP32) && defined(USE_LOGGER)
|
||||
auto previous = esp_log_level_get("*");
|
||||
esp_log_level_set("*", ESP_LOG_NONE);
|
||||
#endif
|
||||
|
||||
for (uint8_t address = 8; address != 120; address++) {
|
||||
auto err = write_readv(address, nullptr, 0, nullptr, 0);
|
||||
if (err == ERROR_OK) {
|
||||
@@ -21,6 +27,9 @@ void I2CBus::i2c_scan_() {
|
||||
// it takes 16sec to scan on nrf52. It prevents board reset.
|
||||
arch_feed_wdt();
|
||||
}
|
||||
#if defined(USE_ESP32) && defined(USE_LOGGER)
|
||||
esp_log_level_set("*", previous);
|
||||
#endif
|
||||
}
|
||||
|
||||
ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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>());
|
||||
|
||||
@@ -155,8 +155,6 @@ SerializationBuffer<> 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);
|
||||
|
||||
@@ -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 {
|
||||
@@ -180,11 +189,10 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
};
|
||||
|
||||
static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void setup_preferences() {
|
||||
s_preferences.open();
|
||||
global_preferences = &s_preferences;
|
||||
auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
prefs->open();
|
||||
global_preferences = prefs;
|
||||
}
|
||||
|
||||
} // namespace libretiny
|
||||
|
||||
@@ -138,20 +138,20 @@ class LambdaLightEffect : public LightEffect {
|
||||
class AutomationLightEffect : public LightEffect {
|
||||
public:
|
||||
AutomationLightEffect(const char *name) : LightEffect(name) {}
|
||||
void stop() override { this->trig_.stop_action(); }
|
||||
void stop() override { this->trig_->stop_action(); }
|
||||
void apply() override {
|
||||
if (!this->trig_.is_action_running()) {
|
||||
this->trig_.trigger();
|
||||
if (!this->trig_->is_action_running()) {
|
||||
this->trig_->trigger();
|
||||
}
|
||||
}
|
||||
Trigger<> *get_trig() { return &this->trig_; }
|
||||
Trigger<> *get_trig() const { return trig_; }
|
||||
|
||||
/// Get the current effect index for use in automations.
|
||||
/// Useful for automations that need to know which effect is running.
|
||||
uint32_t get_current_index() const { return this->get_index(); }
|
||||
|
||||
protected:
|
||||
Trigger<> trig_;
|
||||
Trigger<> *trig_{new Trigger<>};
|
||||
};
|
||||
|
||||
struct StrobeLightEffectColor {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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::lock {
|
||||
|
||||
@@ -85,21 +84,21 @@ LockCall &LockCall::set_state(optional<LockState> state) {
|
||||
this->state_ = state;
|
||||
return *this;
|
||||
}
|
||||
LockCall &LockCall::set_state(const char *state) {
|
||||
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
|
||||
LockCall &LockCall::set_state(const std::string &state) {
|
||||
if (str_equals_case_insensitive(state, "LOCKED")) {
|
||||
this->set_state(LOCK_STATE_LOCKED);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) {
|
||||
} else if (str_equals_case_insensitive(state, "UNLOCKED")) {
|
||||
this->set_state(LOCK_STATE_UNLOCKED);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) {
|
||||
} else if (str_equals_case_insensitive(state, "JAMMED")) {
|
||||
this->set_state(LOCK_STATE_JAMMED);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) {
|
||||
} else if (str_equals_case_insensitive(state, "LOCKING")) {
|
||||
this->set_state(LOCK_STATE_LOCKING);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) {
|
||||
} else if (str_equals_case_insensitive(state, "UNLOCKING")) {
|
||||
this->set_state(LOCK_STATE_UNLOCKING);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("NONE")) == 0) {
|
||||
} else if (str_equals_case_insensitive(state, "NONE")) {
|
||||
this->set_state(LOCK_STATE_NONE);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state);
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state.c_str());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -83,8 +83,7 @@ class LockCall {
|
||||
/// Set the state of the lock device.
|
||||
LockCall &set_state(optional<LockState> state);
|
||||
/// Set the state of the lock device based on a string.
|
||||
LockCall &set_state(const char *state);
|
||||
LockCall &set_state(const std::string &state) { return this->set_state(state.c_str()); }
|
||||
LockCall &set_state(const std::string &state);
|
||||
|
||||
void perform();
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S3,
|
||||
add_idf_sdkconfig_option,
|
||||
get_esp32_variant,
|
||||
require_usb_serial_jtag_secondary,
|
||||
require_vfs_termios,
|
||||
)
|
||||
from esphome.components.libretiny import get_libretiny_component, get_libretiny_family
|
||||
from esphome.components.libretiny.const import (
|
||||
@@ -399,15 +397,9 @@ async def to_code(config):
|
||||
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
|
||||
# Define platform support flags for components that need auto-detection
|
||||
try:
|
||||
uart_selection(USB_SERIAL_JTAG)
|
||||
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
|
||||
# USB Serial JTAG code is compiled when platform supports it.
|
||||
# Enable secondary USB serial JTAG console so the VFS functions are available.
|
||||
if CORE.is_esp32 and config[CONF_HARDWARE_UART] != USB_SERIAL_JTAG:
|
||||
require_usb_serial_jtag_secondary()
|
||||
require_vfs_termios()
|
||||
except cv.Invalid:
|
||||
pass
|
||||
try:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -129,7 +128,22 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
|
||||
//
|
||||
// This function handles format strings stored in flash memory (PROGMEM) to save RAM.
|
||||
// Uses vsnprintf_P to read the format string directly from flash without copying to RAM.
|
||||
// The buffer is used in a special way to avoid allocating extra memory:
|
||||
//
|
||||
// Memory layout during execution:
|
||||
// Step 1: Copy format string from flash to buffer
|
||||
// tx_buffer_: [format_string][null][.....................]
|
||||
// tx_buffer_at_: ------------------^
|
||||
// msg_start: saved here -----------^
|
||||
//
|
||||
// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning
|
||||
// and writes formatted output starting at msg_start position
|
||||
// tx_buffer_: [format_string][null][formatted_message][null]
|
||||
// tx_buffer_at_: -------------------------------------^
|
||||
//
|
||||
// Step 3: Output the formatted message (starting at msg_start)
|
||||
// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start
|
||||
// which points to: [formatted_message][null]
|
||||
//
|
||||
void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
|
||||
va_list args) { // NOLINT
|
||||
@@ -139,25 +153,35 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
RecursionGuard guard(global_recursion_guard_);
|
||||
this->tx_buffer_at_ = 0;
|
||||
|
||||
// Write header, format body directly from flash, and write footer
|
||||
this->write_header_to_buffer_(level, tag, line, nullptr, this->tx_buffer_, &this->tx_buffer_at_,
|
||||
this->tx_buffer_size_);
|
||||
this->format_body_to_buffer_P_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_,
|
||||
reinterpret_cast<PGM_P>(format), args);
|
||||
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
|
||||
// Copy format string from progmem
|
||||
auto *format_pgm_p = reinterpret_cast<const uint8_t *>(format);
|
||||
char ch = '.';
|
||||
while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') {
|
||||
this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++);
|
||||
}
|
||||
|
||||
// Ensure null termination
|
||||
uint16_t null_pos = this->tx_buffer_at_ >= this->tx_buffer_size_ ? this->tx_buffer_size_ - 1 : this->tx_buffer_at_;
|
||||
this->tx_buffer_[null_pos] = '\0';
|
||||
// Buffer full from copying format - RAII guard handles cleanup on return
|
||||
if (this->tx_buffer_at_ >= this->tx_buffer_size_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the offset before calling format_log_to_buffer_with_terminator_
|
||||
// since it will increment tx_buffer_at_ to the end of the formatted string
|
||||
uint16_t msg_start = this->tx_buffer_at_;
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
|
||||
&this->tx_buffer_at_, this->tx_buffer_size_);
|
||||
|
||||
uint16_t msg_length =
|
||||
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
|
||||
|
||||
// Listeners get message first (before console write)
|
||||
#ifdef USE_LOG_LISTENERS
|
||||
for (auto *listener : this->log_listeners_)
|
||||
listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_);
|
||||
listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length);
|
||||
#endif
|
||||
|
||||
// Write to console
|
||||
this->write_tx_buffer_to_console_();
|
||||
// Write to console starting at the msg_start
|
||||
this->write_tx_buffer_to_console_(msg_start, &msg_length);
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
@@ -267,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"
|
||||
@@ -299,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
|
||||
}
|
||||
@@ -307,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)
|
||||
@@ -597,39 +597,30 @@ class Logger : public Component {
|
||||
*buffer_at = pos;
|
||||
}
|
||||
|
||||
// Helper to process vsnprintf return value and strip trailing newlines.
|
||||
// Updates buffer_at with the formatted length, handling truncation:
|
||||
// - When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
|
||||
// - When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
|
||||
__attribute__((always_inline)) static inline void process_vsnprintf_result(const char *buffer, uint16_t *buffer_at,
|
||||
uint16_t remaining, int ret) {
|
||||
if (ret < 0)
|
||||
return; // Encoding error, do not increment buffer_at
|
||||
*buffer_at += (ret >= remaining) ? (remaining - 1) : static_cast<uint16_t>(ret);
|
||||
// Remove all trailing newlines right after formatting
|
||||
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n')
|
||||
(*buffer_at)--;
|
||||
}
|
||||
|
||||
inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format,
|
||||
va_list args) {
|
||||
// Check remaining capacity in the buffer
|
||||
// Get remaining capacity in the buffer
|
||||
if (*buffer_at >= buffer_size)
|
||||
return;
|
||||
const uint16_t remaining = buffer_size - *buffer_at;
|
||||
process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf(buffer + *buffer_at, remaining, format, args));
|
||||
}
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// ESP8266 variant that reads format string directly from flash using vsnprintf_P
|
||||
inline void HOT format_body_to_buffer_P_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, PGM_P format,
|
||||
va_list args) {
|
||||
if (*buffer_at >= buffer_size)
|
||||
return;
|
||||
const uint16_t remaining = buffer_size - *buffer_at;
|
||||
process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf_P(buffer + *buffer_at, remaining, format, args));
|
||||
const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args);
|
||||
|
||||
if (ret < 0) {
|
||||
return; // Encoding error, do not increment buffer_at
|
||||
}
|
||||
|
||||
// Update buffer_at with the formatted length (handle truncation)
|
||||
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
|
||||
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
|
||||
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
|
||||
*buffer_at += formatted_len;
|
||||
|
||||
// Remove all trailing newlines right after formatting
|
||||
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') {
|
||||
(*buffer_at)--;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
|
||||
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||
|
||||
@@ -114,6 +114,9 @@ void Logger::pre_setup() {
|
||||
|
||||
global_logger = this;
|
||||
esp_log_set_vprintf(esp_idf_log_vprintf_);
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
|
||||
esp_log_level_set("*", ESP_LOG_VERBOSE);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -28,10 +28,11 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS])
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
cg.add(var.set_intensity(config[CONF_INTENSITY]))
|
||||
cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE]))
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::max7219 {
|
||||
namespace esphome {
|
||||
namespace max7219 {
|
||||
|
||||
static const char *const TAG = "max7219";
|
||||
|
||||
@@ -114,14 +115,12 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = {
|
||||
};
|
||||
|
||||
float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; }
|
||||
|
||||
MAX7219Component::MAX7219Component(uint8_t num_chips) : num_chips_(num_chips) {
|
||||
this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT
|
||||
memset(this->buffer_, 0, this->num_chips_ * 8);
|
||||
}
|
||||
|
||||
void MAX7219Component::setup() {
|
||||
this->spi_setup();
|
||||
this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT
|
||||
for (uint8_t i = 0; i < this->num_chips_ * 8; i++)
|
||||
this->buffer_[i] = 0;
|
||||
|
||||
// let's assume the user has all 8 digits connected, only important in daisy chained setups anyway
|
||||
this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7);
|
||||
// let's use our own ASCII -> led pattern encoding
|
||||
@@ -230,6 +229,7 @@ void MAX7219Component::set_intensity(uint8_t intensity) {
|
||||
this->intensity_ = intensity;
|
||||
}
|
||||
}
|
||||
void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }
|
||||
|
||||
uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) {
|
||||
char buffer[64];
|
||||
@@ -240,4 +240,5 @@ uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time
|
||||
}
|
||||
uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); }
|
||||
|
||||
} // namespace esphome::max7219
|
||||
} // namespace max7219
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
namespace esphome::max7219 {
|
||||
namespace esphome {
|
||||
namespace max7219 {
|
||||
|
||||
class MAX7219Component;
|
||||
|
||||
@@ -16,8 +17,6 @@ class MAX7219Component : public PollingComponent,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
explicit MAX7219Component(uint8_t num_chips);
|
||||
|
||||
void set_writer(max7219_writer_t &&writer);
|
||||
|
||||
void setup() override;
|
||||
@@ -31,6 +30,7 @@ class MAX7219Component : public PollingComponent,
|
||||
void display();
|
||||
|
||||
void set_intensity(uint8_t intensity);
|
||||
void set_num_chips(uint8_t num_chips);
|
||||
void set_reverse(bool reverse) { this->reverse_ = reverse; };
|
||||
|
||||
/// Evaluate the printf-format and print the result at the given position.
|
||||
@@ -56,9 +56,10 @@ class MAX7219Component : public PollingComponent,
|
||||
uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most)
|
||||
bool intensity_changed_{}; // True if we need to re-send the intensity
|
||||
uint8_t num_chips_{1};
|
||||
uint8_t *buffer_{nullptr};
|
||||
uint8_t *buffer_;
|
||||
bool reverse_{false};
|
||||
max7219_writer_t writer_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::max7219
|
||||
} // namespace max7219
|
||||
} // namespace esphome
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
namespace media_player {
|
||||
@@ -108,25 +107,25 @@ MediaPlayerCall &MediaPlayerCall::set_command(optional<MediaPlayerCommand> comma
|
||||
this->command_ = command;
|
||||
return *this;
|
||||
}
|
||||
MediaPlayerCall &MediaPlayerCall::set_command(const char *command) {
|
||||
if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PLAY")) == 0) {
|
||||
MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) {
|
||||
if (str_equals_case_insensitive(command, "PLAY")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_PLAY);
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PAUSE")) == 0) {
|
||||
} else if (str_equals_case_insensitive(command, "PAUSE")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_PAUSE);
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
|
||||
} else if (str_equals_case_insensitive(command, "STOP")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_STOP);
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("MUTE")) == 0) {
|
||||
} else if (str_equals_case_insensitive(command, "MUTE")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_MUTE);
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNMUTE")) == 0) {
|
||||
} else if (str_equals_case_insensitive(command, "UNMUTE")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE);
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
|
||||
} else if (str_equals_case_insensitive(command, "TOGGLE")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE);
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_ON")) == 0) {
|
||||
} else if (str_equals_case_insensitive(command, "TURN_ON")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON);
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) {
|
||||
} else if (str_equals_case_insensitive(command, "TURN_OFF")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -114,8 +114,7 @@ class MediaPlayerCall {
|
||||
|
||||
MediaPlayerCall &set_command(MediaPlayerCommand command);
|
||||
MediaPlayerCall &set_command(optional<MediaPlayerCommand> command);
|
||||
MediaPlayerCall &set_command(const char *command);
|
||||
MediaPlayerCall &set_command(const std::string &command) { return this->set_command(command.c_str()); }
|
||||
MediaPlayerCall &set_command(const std::string &command);
|
||||
|
||||
MediaPlayerCall &set_media_url(const std::string &url);
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -325,7 +325,7 @@ void MicroWakeWord::loop() {
|
||||
ESP_LOGD(TAG, "Detected '%s' with sliding average probability is %.2f and max probability is %.2f",
|
||||
detection_event.wake_word->c_str(), (detection_event.average_probability / uint8_to_float_divisor),
|
||||
(detection_event.max_probability / uint8_to_float_divisor));
|
||||
this->wake_word_detected_trigger_.trigger(*detection_event.wake_word);
|
||||
this->wake_word_detected_trigger_->trigger(*detection_event.wake_word);
|
||||
if (this->stop_after_detection_) {
|
||||
this->stop();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user