mirror of
https://github.com/esphome/esphome.git
synced 2025-11-16 06:45:48 +00:00
Compare commits
18 Commits
template_a
...
reduce_log
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d60c358f48 | ||
|
|
26b820272a | ||
|
|
9557c90c20 | ||
|
|
d64bcf27b3 | ||
|
|
d5d61546e7 | ||
|
|
554cdbd5a4 | ||
|
|
0d147e5d10 | ||
|
|
730a70ee8b | ||
|
|
d096f1192d | ||
|
|
88a23acc4b | ||
|
|
950dff1a38 | ||
|
|
61eddfdcda | ||
|
|
1df996601d | ||
|
|
c32891ec02 | ||
|
|
2bf6d48fcf | ||
|
|
e49a943cf7 | ||
|
|
67524e14ee | ||
|
|
2290eb0dd2 |
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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.4
|
||||
rev: v0.14.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -52,8 +52,10 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
|
||||
}
|
||||
|
||||
static const LogString *color_mode_to_human(ColorMode color_mode) {
|
||||
if (color_mode == ColorMode::UNKNOWN)
|
||||
return LOG_STR("Unknown");
|
||||
if (color_mode == ColorMode::ON_OFF)
|
||||
return LOG_STR("On/Off");
|
||||
if (color_mode == ColorMode::BRIGHTNESS)
|
||||
return LOG_STR("Brightness");
|
||||
if (color_mode == ColorMode::WHITE)
|
||||
return LOG_STR("White");
|
||||
if (color_mode == ColorMode::COLOR_TEMPERATURE)
|
||||
@@ -68,7 +70,7 @@ static const LogString *color_mode_to_human(ColorMode color_mode) {
|
||||
return LOG_STR("RGB + cold/warm white");
|
||||
if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE)
|
||||
return LOG_STR("RGB + color temperature");
|
||||
return LOG_STR("");
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
|
||||
// Helper to log percentage values
|
||||
|
||||
@@ -365,8 +365,10 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
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")
|
||||
try:
|
||||
uart_selection(USB_SERIAL_JTAG)
|
||||
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
|
||||
|
||||
@@ -65,7 +65,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
uint16_t buffer_at = 0; // Initialize buffer position
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
|
||||
MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer);
|
||||
// Add newline if platform needs it (ESP32 doesn't add via write_msg_)
|
||||
this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer, buffer_at);
|
||||
}
|
||||
|
||||
// Reset the recursion guard for this task
|
||||
@@ -131,18 +133,19 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
|
||||
// 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
|
||||
uint32_t msg_start = this->tx_buffer_at_;
|
||||
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_);
|
||||
|
||||
// Write to console and send callback starting at the msg_start
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_ + msg_start);
|
||||
}
|
||||
size_t msg_length =
|
||||
uint16_t msg_length =
|
||||
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
|
||||
|
||||
// Callbacks get message first (before console write)
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
|
||||
|
||||
// Write to console starting at the msg_start
|
||||
this->write_tx_buffer_to_console_(msg_start, &msg_length);
|
||||
|
||||
global_recursion_guard_ = false;
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
@@ -209,9 +212,7 @@ void Logger::process_messages_() {
|
||||
// This ensures all log messages appear on the console in a clean, serialized manner
|
||||
// Note: Messages may appear slightly out of order due to async processing, but
|
||||
// this is preferred over corrupted/interleaved console output
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_);
|
||||
}
|
||||
this->write_tx_buffer_to_console_();
|
||||
}
|
||||
} else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
|
||||
@@ -71,6 +71,17 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
// Platform-specific: does write_msg_ add its own newline?
|
||||
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266)
|
||||
// Allows single write call with newline included for efficiency
|
||||
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
|
||||
// Newline should NOT be added to buffer
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266)
|
||||
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
|
||||
#else
|
||||
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
/** Enum for logging UART selection
|
||||
*
|
||||
@@ -173,7 +184,7 @@ class Logger : public Component {
|
||||
|
||||
protected:
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg);
|
||||
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)
|
||||
@@ -200,6 +211,35 @@ class Logger : public Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to add newline to buffer for platforms that need it
|
||||
// Modifies buffer_at to include the newline
|
||||
inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
|
||||
if constexpr (!WRITE_MSG_ADDS_NEWLINE) {
|
||||
// Add newline - don't need to maintain null termination
|
||||
// write_msg_ now always receives explicit length, so we can safely overwrite the null terminator
|
||||
// This is safe because:
|
||||
// 1. Callbacks already received the message (before we add newline)
|
||||
// 2. write_msg_ receives the length explicitly (doesn't need null terminator)
|
||||
if (*buffer_at < buffer_size) {
|
||||
buffer[(*buffer_at)++] = '\n';
|
||||
} else if (buffer_size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
buffer[buffer_size - 1] = '\n';
|
||||
*buffer_at = buffer_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to write tx_buffer_ to console if logging is enabled
|
||||
// INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null
|
||||
inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) {
|
||||
if (this->baud_rate_ > 0) {
|
||||
uint16_t *len_ptr = length ? length : &this->tx_buffer_at_;
|
||||
this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset);
|
||||
this->write_msg_(this->tx_buffer_ + offset, *len_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to format and send a log message to both console and callbacks
|
||||
inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args) {
|
||||
@@ -208,10 +248,11 @@ class Logger : public Component {
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_,
|
||||
this->tx_buffer_size_);
|
||||
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
|
||||
}
|
||||
// Callbacks get message WITHOUT newline (for API/MQTT/syslog)
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
|
||||
|
||||
// Console gets message WITH newline (if platform needs it)
|
||||
this->write_tx_buffer_to_console_();
|
||||
}
|
||||
|
||||
// Write the body of the log message to the buffer
|
||||
@@ -425,7 +466,9 @@ class Logger : public Component {
|
||||
}
|
||||
|
||||
// Update buffer_at with the formatted length (handle truncation)
|
||||
uint16_t formatted_len = (ret >= remaining) ? remaining : ret;
|
||||
// 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
|
||||
|
||||
@@ -121,25 +121,23 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
if (
|
||||
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
|
||||
this->uart_ == UART_SELECTION_USB_CDC
|
||||
#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
|
||||
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
|
||||
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
// Length is now always passed explicitly - no strlen() fallback needed
|
||||
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
|
||||
// USB CDC/JTAG - single write including newline (already in buffer)
|
||||
// Use fwrite to stdout which goes through VFS to USB console
|
||||
//
|
||||
// Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG).
|
||||
// They are ONLY defined when the user explicitly selects USB as the logger output in their config.
|
||||
// This is compile-time selection, not runtime detection - if USB is configured, it's always used.
|
||||
// There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility
|
||||
// to configure correctly for their hardware. This approach eliminates runtime overhead.
|
||||
fwrite(msg, 1, len, stdout);
|
||||
#else
|
||||
/* DISABLES CODE */ (false) // NOLINT
|
||||
// Regular UART - single write including newline (already in buffer)
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
#endif
|
||||
) {
|
||||
puts(msg);
|
||||
} else {
|
||||
// Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
|
||||
size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
uart_write_bytes(this->uart_num_, "\n", 1);
|
||||
}
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
|
||||
@@ -33,7 +33,10 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
// Single write with newline already in buffer (added by caller)
|
||||
this->hw_serial_->write(msg, len);
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
@@ -49,7 +49,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -27,7 +27,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -62,7 +62,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
|
||||
@@ -137,11 +137,7 @@ async def to_code(config):
|
||||
cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME]))
|
||||
supports_arm_night = True
|
||||
|
||||
if sensors := config.get(CONF_BINARY_SENSORS, []):
|
||||
# Initialize FixedVector with the exact number of sensors
|
||||
cg.add(var.init_sensors(len(sensors)))
|
||||
|
||||
for sensor in sensors:
|
||||
for sensor in config.get(CONF_BINARY_SENSORS, []):
|
||||
bs = await cg.get_variable(sensor[CONF_INPUT])
|
||||
|
||||
flags = BinarySensorFlags[FLAG_NORMAL]
|
||||
|
||||
@@ -20,13 +20,10 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
|
||||
// Save the flags and type. Assign a store index for the per sensor data type.
|
||||
SensorDataStore sd;
|
||||
sd.last_chime_state = false;
|
||||
AlarmSensor alarm_sensor;
|
||||
alarm_sensor.sensor = sensor;
|
||||
alarm_sensor.info.flags = flags;
|
||||
alarm_sensor.info.type = type;
|
||||
alarm_sensor.info.store_index = this->next_store_index_++;
|
||||
this->sensors_.push_back(alarm_sensor);
|
||||
this->sensor_map_[sensor].flags = flags;
|
||||
this->sensor_map_[sensor].type = type;
|
||||
this->sensor_data_.push_back(sd);
|
||||
this->sensor_map_[sensor].store_index = this->next_store_index_++;
|
||||
};
|
||||
|
||||
static const LogString *sensor_type_to_string(AlarmSensorType type) {
|
||||
@@ -48,7 +45,7 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"TemplateAlarmControlPanel:\n"
|
||||
" Current State: %s\n"
|
||||
" Number of Codes: %zu\n"
|
||||
" Number of Codes: %u\n"
|
||||
" Requires Code To Arm: %s\n"
|
||||
" Arming Away Time: %" PRIu32 "s\n"
|
||||
" Arming Home Time: %" PRIu32 "s\n"
|
||||
@@ -61,8 +58,7 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
(this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000),
|
||||
(this->trigger_time_ / 1000), this->get_supported_features());
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (const auto &alarm_sensor : this->sensors_) {
|
||||
const uint16_t flags = alarm_sensor.info.flags;
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Binary Sensor:\n"
|
||||
" Name: %s\n"
|
||||
@@ -71,10 +67,11 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
" Armed night bypass: %s\n"
|
||||
" Auto bypass: %s\n"
|
||||
" Chime mode: %s",
|
||||
alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)),
|
||||
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
|
||||
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
|
||||
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME));
|
||||
sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -124,9 +121,7 @@ void TemplateAlarmControlPanel::loop() {
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// Test all of the sensors regardless of the alarm panel state
|
||||
for (const auto &alarm_sensor : this->sensors_) {
|
||||
const auto &info = alarm_sensor.info;
|
||||
auto *sensor = alarm_sensor.sensor;
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
// Check for chime zones
|
||||
if (info.flags & BINARY_SENSOR_MODE_CHIME) {
|
||||
// Look for the transition from closed to open
|
||||
@@ -247,11 +242,11 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
|
||||
|
||||
void TemplateAlarmControlPanel::bypass_before_arming() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (const auto &alarm_sensor : this->sensors_) {
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
// Check for faulted bypass_auto sensors and remove them from monitoring
|
||||
if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) {
|
||||
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str());
|
||||
this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index);
|
||||
if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) {
|
||||
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str());
|
||||
this->bypassed_sensor_indicies_.push_back(info.store_index);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
|
||||
|
||||
@@ -50,13 +49,6 @@ struct SensorInfo {
|
||||
uint8_t store_index;
|
||||
};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct AlarmSensor {
|
||||
binary_sensor::BinarySensor *sensor;
|
||||
SensorInfo info;
|
||||
};
|
||||
#endif
|
||||
|
||||
class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component {
|
||||
public:
|
||||
TemplateAlarmControlPanel();
|
||||
@@ -71,12 +63,6 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
void bypass_before_arming();
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
/** Initialize the sensors vector with the specified capacity.
|
||||
*
|
||||
* @param capacity The number of sensors to allocate space for.
|
||||
*/
|
||||
void init_sensors(size_t capacity) { this->sensors_.init(capacity); }
|
||||
|
||||
/** Add a binary_sensor to the alarm_panel.
|
||||
*
|
||||
* @param sensor The BinarySensor instance.
|
||||
@@ -136,8 +122,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
protected:
|
||||
void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// List of binary sensors with their alarm-specific info
|
||||
FixedVector<AlarmSensor> sensors_;
|
||||
// This maps a binary sensor to its alarm specific info
|
||||
std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
|
||||
// a list of automatically bypassed sensors
|
||||
std::vector<uint8_t> bypassed_sensor_indicies_;
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from logging import getLogger
|
||||
import math
|
||||
import re
|
||||
|
||||
@@ -35,6 +36,8 @@ from esphome.core import CORE, ID
|
||||
import esphome.final_validate as fv
|
||||
from esphome.yaml_util import make_data_base
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
uart_ns = cg.esphome_ns.namespace("uart")
|
||||
UARTComponent = uart_ns.class_("UARTComponent")
|
||||
@@ -130,6 +133,21 @@ def validate_host_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def validate_rx_buffer_size(config):
|
||||
if CORE.is_esp32:
|
||||
# ESP32 UART hardware FIFO is 128 bytes (LP UART is 16 bytes, but we use 128 as safe minimum)
|
||||
# rx_buffer_size must be greater than the hardware FIFO length
|
||||
min_buffer_size = 128
|
||||
if config[CONF_RX_BUFFER_SIZE] <= min_buffer_size:
|
||||
_LOGGER.warning(
|
||||
"UART rx_buffer_size (%d bytes) is too small and must be greater than the hardware "
|
||||
"FIFO size (%d bytes). The buffer size will be automatically adjusted at runtime.",
|
||||
config[CONF_RX_BUFFER_SIZE],
|
||||
min_buffer_size,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _uart_declare_type(value):
|
||||
if CORE.is_esp8266:
|
||||
return cv.declare_id(ESP8266UartComponent)(value)
|
||||
@@ -247,6 +265,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT),
|
||||
validate_host_config,
|
||||
validate_rx_buffer_size,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -91,6 +91,16 @@ void IDFUARTComponent::setup() {
|
||||
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
|
||||
this->lock_ = xSemaphoreCreateMutex();
|
||||
|
||||
#if (SOC_UART_LP_NUM >= 1)
|
||||
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
|
||||
#else
|
||||
size_t fifo_len = SOC_UART_FIFO_LEN;
|
||||
#endif
|
||||
if (this->rx_buffer_size_ <= fifo_len) {
|
||||
ESP_LOGW(TAG, "rx_buffer_size is too small, must be greater than %zu", fifo_len);
|
||||
this->rx_buffer_size_ = fifo_len * 2;
|
||||
}
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
|
||||
this->load_settings(false);
|
||||
@@ -237,8 +247,12 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
|
||||
|
||||
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
uart_write_bytes(this->uart_num_, data, len);
|
||||
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
|
||||
xSemaphoreGive(this->lock_);
|
||||
if (write_len != (int32_t) len) {
|
||||
ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len);
|
||||
this->mark_failed();
|
||||
}
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
|
||||
@@ -267,6 +281,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
|
||||
bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
size_t length_to_read = len;
|
||||
int32_t read_len = 0;
|
||||
if (!this->check_read_timeout_(len))
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
@@ -277,25 +292,31 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
this->has_peek_ = false;
|
||||
}
|
||||
if (length_to_read > 0)
|
||||
uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
|
||||
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
|
||||
xSemaphoreGive(this->lock_);
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
return read_len == (int32_t) length_to_read;
|
||||
}
|
||||
|
||||
int IDFUARTComponent::available() {
|
||||
size_t available;
|
||||
size_t available = 0;
|
||||
esp_err_t err;
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
uart_get_buffered_data_len(this->uart_num_, &available);
|
||||
if (this->has_peek_)
|
||||
available++;
|
||||
err = uart_get_buffered_data_len(this->uart_num_, &available);
|
||||
xSemaphoreGive(this->lock_);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
}
|
||||
if (this->has_peek_) {
|
||||
available++;
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from esphome.components.network import (
|
||||
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import only_with_esp_idf
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
CONF_BSSID,
|
||||
@@ -352,7 +351,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_USE_PSRAM): cv.All(
|
||||
only_with_esp_idf, cv.requires_component("psram"), cv.boolean
|
||||
cv.only_on_esp32, cv.requires_component("psram"), cv.boolean
|
||||
),
|
||||
}
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.2
|
||||
pylint==4.0.3
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.4 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.5 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
esphome:
|
||||
name: template-alarm-many-sensors
|
||||
friendly_name: "Template Alarm Control Panel with Many Sensors"
|
||||
|
||||
logger:
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: sensor1
|
||||
name: "Door 1"
|
||||
- platform: template
|
||||
id: sensor2
|
||||
name: "Door 2"
|
||||
- platform: template
|
||||
id: sensor3
|
||||
name: "Window 1"
|
||||
- platform: template
|
||||
id: sensor4
|
||||
name: "Window 2"
|
||||
- platform: template
|
||||
id: sensor5
|
||||
name: "Motion 1"
|
||||
- platform: template
|
||||
id: sensor6
|
||||
name: "Motion 2"
|
||||
- platform: template
|
||||
id: sensor7
|
||||
name: "Glass Break 1"
|
||||
- platform: template
|
||||
id: sensor8
|
||||
name: "Glass Break 2"
|
||||
- platform: template
|
||||
id: sensor9
|
||||
name: "Smoke Detector"
|
||||
- platform: template
|
||||
id: sensor10
|
||||
name: "CO Detector"
|
||||
|
||||
alarm_control_panel:
|
||||
- platform: template
|
||||
id: test_alarm
|
||||
name: "Test Alarm"
|
||||
codes:
|
||||
- "1234"
|
||||
requires_code_to_arm: true
|
||||
arming_away_time: 5s
|
||||
arming_home_time: 3s
|
||||
arming_night_time: 3s
|
||||
pending_time: 10s
|
||||
trigger_time: 300s
|
||||
restore_mode: ALWAYS_DISARMED
|
||||
binary_sensors:
|
||||
- input: sensor1
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: true
|
||||
chime: true
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor2
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: true
|
||||
chime: true
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor3
|
||||
bypass_armed_home: true
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor4
|
||||
bypass_armed_home: true
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor5
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: true
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor6
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: true
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor7
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor8
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor9
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT_ALWAYS
|
||||
- input: sensor10
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT_ALWAYS
|
||||
on_disarmed:
|
||||
- logger.log: "Alarm disarmed"
|
||||
on_arming:
|
||||
- logger.log: "Alarm arming"
|
||||
on_armed_away:
|
||||
- logger.log: "Alarm armed away"
|
||||
on_armed_home:
|
||||
- logger.log: "Alarm armed home"
|
||||
on_armed_night:
|
||||
- logger.log: "Alarm armed night"
|
||||
on_pending:
|
||||
- logger.log: "Alarm pending"
|
||||
on_triggered:
|
||||
- logger.log: "Alarm triggered"
|
||||
on_cleared:
|
||||
- logger.log: "Alarm cleared"
|
||||
on_chime:
|
||||
- logger.log: "Chime activated"
|
||||
on_ready:
|
||||
- logger.log: "Sensors ready state changed"
|
||||
@@ -1,118 +0,0 @@
|
||||
"""Integration test for template alarm control panel with many sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi.model import APIIntEnum
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
class EspHomeACPFeatures(APIIntEnum):
|
||||
"""ESPHome AlarmControlPanel feature numbers."""
|
||||
|
||||
ARM_HOME = 1
|
||||
ARM_AWAY = 2
|
||||
ARM_NIGHT = 4
|
||||
TRIGGER = 8
|
||||
ARM_CUSTOM_BYPASS = 16
|
||||
ARM_VACATION = 32
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_template_alarm_control_panel_many_sensors(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test template alarm control panel with 10 binary sensors using FixedVector."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Get entity info first
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the alarm control panel and binary sensors
|
||||
alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None
|
||||
binary_sensors: list[aioesphomeapi.BinarySensorInfo] = []
|
||||
|
||||
for entity in entities:
|
||||
if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo):
|
||||
alarm_info = entity
|
||||
elif isinstance(entity, aioesphomeapi.BinarySensorInfo):
|
||||
binary_sensors.append(entity)
|
||||
|
||||
assert alarm_info is not None, "Alarm control panel entity info not found"
|
||||
assert alarm_info.name == "Test Alarm"
|
||||
assert alarm_info.requires_code is True
|
||||
assert alarm_info.requires_code_to_arm is True
|
||||
|
||||
# Verify we have 10 binary sensors
|
||||
assert len(binary_sensors) == 10, (
|
||||
f"Expected 10 binary sensors, got {len(binary_sensors)}"
|
||||
)
|
||||
|
||||
# Verify sensor names
|
||||
expected_sensor_names = {
|
||||
"Door 1",
|
||||
"Door 2",
|
||||
"Window 1",
|
||||
"Window 2",
|
||||
"Motion 1",
|
||||
"Motion 2",
|
||||
"Glass Break 1",
|
||||
"Glass Break 2",
|
||||
"Smoke Detector",
|
||||
"CO Detector",
|
||||
}
|
||||
actual_sensor_names = {sensor.name for sensor in binary_sensors}
|
||||
assert actual_sensor_names == expected_sensor_names, (
|
||||
f"Sensor names mismatch. Expected: {expected_sensor_names}, "
|
||||
f"Got: {actual_sensor_names}"
|
||||
)
|
||||
|
||||
# Use InitialStateHelper to wait for all initial states
|
||||
state_helper = InitialStateHelper(entities)
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
# We'll receive subsequent states here after initial states
|
||||
pass
|
||||
|
||||
client.subscribe_states(state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for all initial states
|
||||
await state_helper.wait_for_initial_states(timeout=5.0)
|
||||
|
||||
# Verify the alarm state is disarmed initially
|
||||
alarm_state = state_helper.initial_states.get(alarm_info.key)
|
||||
assert alarm_state is not None, "Alarm control panel initial state not received"
|
||||
assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState)
|
||||
assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, (
|
||||
f"Expected initial state DISARMED, got {alarm_state.state}"
|
||||
)
|
||||
|
||||
# Verify all 10 binary sensors have initial states
|
||||
binary_sensor_states = [
|
||||
state_helper.initial_states.get(sensor.key) for sensor in binary_sensors
|
||||
]
|
||||
assert all(state is not None for state in binary_sensor_states), (
|
||||
"Not all binary sensors have initial states"
|
||||
)
|
||||
|
||||
# Verify all binary sensor states are BinarySensorState type
|
||||
for i, state in enumerate(binary_sensor_states):
|
||||
assert isinstance(state, aioesphomeapi.BinarySensorState), (
|
||||
f"Binary sensor {i} state is not BinarySensorState: {type(state)}"
|
||||
)
|
||||
|
||||
# Verify supported features
|
||||
expected_features = (
|
||||
EspHomeACPFeatures.ARM_HOME
|
||||
| EspHomeACPFeatures.ARM_AWAY
|
||||
| EspHomeACPFeatures.ARM_NIGHT
|
||||
| EspHomeACPFeatures.TRIGGER
|
||||
)
|
||||
assert alarm_info.supported_features == expected_features, (
|
||||
f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), "
|
||||
f"got {alarm_info.supported_features}"
|
||||
)
|
||||
Reference in New Issue
Block a user