mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 16:51:52 +00:00
Compare commits
6 Commits
rtttl_prog
...
logger_thr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30f9bfaf83 | ||
|
|
eb7aa3420f | ||
|
|
86f91eed2f | ||
|
|
41cecbfb0f | ||
|
|
9315da79bc | ||
|
|
155447f541 |
@@ -1 +1 @@
|
||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
||||
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
|
||||
|
||||
@@ -66,7 +66,7 @@ async def to_code(config):
|
||||
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
|
||||
|
||||
# DSMR Parser
|
||||
cg.add_library("esphome/dsmr_parser", "1.0.0")
|
||||
cg.add_library("esphome/dsmr_parser", "1.1.0")
|
||||
|
||||
# Crypto
|
||||
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0")
|
||||
|
||||
@@ -718,14 +718,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("fw_core_version"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("fw_module_version"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_core_version"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_module_version"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("telegram"): text_sensor.text_sensor_schema().extend(
|
||||
{cv.Optional(CONF_INTERNAL, default=True): cv.boolean}
|
||||
),
|
||||
|
||||
@@ -36,8 +36,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
#endif
|
||||
|
||||
// Fast path: main thread, no recursion (99.9% of all logs)
|
||||
// Pass nullptr for thread_name since we already know this is the main task
|
||||
if (is_main_task && !this->main_task_recursion_guard_) [[likely]] {
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args);
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,21 +48,23 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
}
|
||||
|
||||
// Non-main thread handling (~0.1% of logs)
|
||||
// Resolve thread name once and pass it through the logging chain.
|
||||
// ESP32/LibreTiny: use TaskHandle_t overload to avoid redundant xTaskGetCurrentTaskHandle()
|
||||
// (we already have the handle from the main task check above).
|
||||
// Host: pass a stack buffer for pthread_getname_np to write into.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task);
|
||||
const char *thread_name = get_thread_name_(current_task);
|
||||
#else // USE_HOST
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args);
|
||||
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||
#endif
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name);
|
||||
}
|
||||
|
||||
// Handles non-main thread logging only
|
||||
// Kept separate from hot path to improve instruction cache performance
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
TaskHandle_t current_task) {
|
||||
#else // USE_HOST
|
||||
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) {
|
||||
#endif
|
||||
const char *thread_name) {
|
||||
// Check if already in recursion for this non-main thread/task
|
||||
if (this->is_non_main_task_recursive_()) {
|
||||
return;
|
||||
@@ -73,12 +76,8 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
bool message_sent = false;
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// For non-main threads/tasks, queue the message for callbacks
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
message_sent =
|
||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args);
|
||||
#else // USE_HOST
|
||||
message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args);
|
||||
#endif
|
||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args);
|
||||
if (message_sent) {
|
||||
// Enable logger loop to process the buffered message
|
||||
// This is safe to call from any context including ISRs
|
||||
@@ -101,19 +100,27 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
#endif
|
||||
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
||||
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||
this->write_to_console_(buf);
|
||||
}
|
||||
|
||||
// RAII guard automatically resets on return
|
||||
}
|
||||
#else
|
||||
// Implementation for all other platforms (single-task, no threading)
|
||||
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
|
||||
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
|
||||
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
||||
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
|
||||
#ifdef USE_ZEPHYR
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
|
||||
this->get_thread_name_(tmp));
|
||||
#else // Other single-task platforms don't have thread names, so pass nullptr
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
#endif
|
||||
}
|
||||
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
|
||||
|
||||
@@ -129,7 +136,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdarg>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
#include <pthread.h>
|
||||
@@ -124,6 +125,10 @@ 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;
|
||||
|
||||
// Stack buffer size for retrieving thread/task names from the OS
|
||||
// macOS allows up to 64 bytes, Linux up to 16
|
||||
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
@@ -408,34 +413,24 @@ class Logger : public Component {
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
// Handles non-main thread logging only (~0.1% of calls)
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice
|
||||
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
TaskHandle_t current_task);
|
||||
#else // USE_HOST
|
||||
// Host: No task handle parameter needed (not used in send_message_thread_safe)
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args);
|
||||
#endif
|
||||
const char *thread_name);
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on)
|
||||
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args, LogBuffer &buf) {
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST)
|
||||
buf.write_header(level, tag, line, this->get_thread_name_());
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
buf.write_header(level, tag, line, this->get_thread_name_(tmp));
|
||||
#else
|
||||
buf.write_header(level, tag, line, nullptr);
|
||||
#endif
|
||||
va_list args, LogBuffer &buf, const char *thread_name) {
|
||||
buf.write_header(level, tag, line, thread_name);
|
||||
buf.format_body(format, args);
|
||||
}
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Format a log message with flash string format and write it to a buffer with header, footer, and null terminator
|
||||
// ESP8266-only (single-task), thread_name is always nullptr
|
||||
inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
|
||||
const __FlashStringHelper *format, va_list args,
|
||||
LogBuffer &buf) {
|
||||
@@ -466,9 +461,10 @@ class Logger : public Component {
|
||||
|
||||
// Helper to format and send a log message to both console and listeners
|
||||
// Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings
|
||||
// thread_name: name of the calling thread/task, or nullptr for main task
|
||||
template<typename FormatType>
|
||||
inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line,
|
||||
FormatType format, va_list args) {
|
||||
FormatType format, va_list args, const char *thread_name) {
|
||||
RecursionGuard guard(recursion_guard);
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
@@ -477,7 +473,7 @@ class Logger : public Component {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||
}
|
||||
this->notify_listeners_(level, tag, buf);
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
@@ -565,37 +561,57 @@ class Logger : public Component {
|
||||
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(
|
||||
#ifdef USE_ZEPHYR
|
||||
char *buff
|
||||
// --- get_thread_name_ overloads (per-platform) ---
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls
|
||||
// when the caller already has the handle (e.g. from the main task check in log_vprintf_)
|
||||
const char *get_thread_name_(TaskHandle_t task) {
|
||||
if (task == this->main_task_) {
|
||||
return nullptr; // Main task
|
||||
}
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(task);
|
||||
#endif
|
||||
) {
|
||||
#ifdef USE_ZEPHYR
|
||||
}
|
||||
|
||||
// Convenience overload - gets the current task handle and delegates
|
||||
const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); }
|
||||
|
||||
#elif defined(USE_HOST)
|
||||
// Takes a caller-provided buffer for the thread name (stack-allocated for thread safety)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, get the thread name into the caller-provided buffer
|
||||
if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) {
|
||||
return buff.data();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
k_tid_t current_task = k_current_get();
|
||||
#else
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
#endif
|
||||
if (current_task == main_task_) {
|
||||
return nullptr; // Main task
|
||||
} else {
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(current_task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
|
||||
return buff;
|
||||
#endif
|
||||
}
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff.data(), buff.size(), "%p", current_task);
|
||||
return buff.data();
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Non-main task recursion guards (per-platform) ---
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
// RAII guard for non-main task recursion using pthread TLS
|
||||
class NonMainTaskRecursionGuard {
|
||||
@@ -635,22 +651,6 @@ class Logger : public Component {
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_HOST
|
||||
const char *HOT get_thread_name_() {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, return the thread name
|
||||
// We store it in thread-local storage to avoid allocation
|
||||
static thread_local char thread_name_buf[32];
|
||||
if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
return thread_name_buf;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
||||
inline void disable_loop_when_buffer_empty_() {
|
||||
|
||||
@@ -59,7 +59,7 @@ void TaskLogBuffer::release_message_main_loop(void *token) {
|
||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
@@ -95,7 +95,6 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
// Store the thread name now instead of waiting until main loop processing
|
||||
// This avoids crashes if the task completes or is deleted between when this message
|
||||
// is enqueued and when it's processed by the main loop
|
||||
const char *thread_name = pcTaskGetName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
|
||||
@@ -58,7 +58,7 @@ class TaskLogBuffer {
|
||||
void release_message_main_loop(void *token);
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||
|
||||
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format,
|
||||
va_list args) {
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// Acquire a slot
|
||||
int slot_index = this->acquire_write_slot_();
|
||||
if (slot_index < 0) {
|
||||
@@ -85,11 +85,9 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
|
||||
msg.tag = tag;
|
||||
msg.line = line;
|
||||
|
||||
// Get thread name using pthread
|
||||
char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE];
|
||||
// pthread_getname_np works the same on Linux and macOS
|
||||
if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1);
|
||||
// Store the thread name now to avoid crashes if thread exits before processing
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
|
||||
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
|
||||
} else {
|
||||
msg.thread_name[0] = '\0';
|
||||
|
||||
@@ -86,7 +86,8 @@ class TaskLogBufferHost {
|
||||
|
||||
// Thread-safe - send a message to the buffer from any thread
|
||||
// Returns true if message was queued, false if buffer is full
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args);
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed
|
||||
inline bool HOT has_messages() const {
|
||||
|
||||
@@ -101,7 +101,7 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
|
||||
}
|
||||
|
||||
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
|
||||
TaskHandle_t task_handle, const char *format, va_list args) {
|
||||
const char *thread_name, const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
@@ -162,7 +162,6 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char
|
||||
msg->line = line;
|
||||
|
||||
// Store the thread name now to avoid crashes if task is deleted before processing
|
||||
const char *thread_name = pcTaskGetTaskName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
|
||||
|
||||
@@ -70,7 +70,7 @@ class TaskLogBufferLibreTiny {
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Fast check using volatile counter - no lock needed
|
||||
|
||||
@@ -13,31 +13,12 @@ static const char *const TAG = "mqtt.alarm_control_panel";
|
||||
|
||||
using namespace esphome::alarm_control_panel;
|
||||
|
||||
// Alarm state MQTT strings indexed by AlarmControlPanelState enum (0-9)
|
||||
PROGMEM_STRING_TABLE(AlarmMqttStateStrings, "disarmed", "armed_home", "armed_away", "armed_night", "armed_vacation",
|
||||
"armed_custom_bypass", "pending", "arming", "disarming", "triggered", "unknown");
|
||||
|
||||
static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) {
|
||||
switch (state) {
|
||||
case ACP_STATE_DISARMED:
|
||||
return ESPHOME_F("disarmed");
|
||||
case ACP_STATE_ARMED_HOME:
|
||||
return ESPHOME_F("armed_home");
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
return ESPHOME_F("armed_away");
|
||||
case ACP_STATE_ARMED_NIGHT:
|
||||
return ESPHOME_F("armed_night");
|
||||
case ACP_STATE_ARMED_VACATION:
|
||||
return ESPHOME_F("armed_vacation");
|
||||
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||
return ESPHOME_F("armed_custom_bypass");
|
||||
case ACP_STATE_PENDING:
|
||||
return ESPHOME_F("pending");
|
||||
case ACP_STATE_ARMING:
|
||||
return ESPHOME_F("arming");
|
||||
case ACP_STATE_DISARMING:
|
||||
return ESPHOME_F("disarming");
|
||||
case ACP_STATE_TRIGGERED:
|
||||
return ESPHOME_F("triggered");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return AlarmMqttStateStrings::get_progmem_str(static_cast<uint8_t>(state), AlarmMqttStateStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/version.h"
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
@@ -27,6 +28,11 @@ namespace esphome::mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt";
|
||||
|
||||
// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8)
|
||||
PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version",
|
||||
"Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized",
|
||||
"Not Enough Space", "TLS Bad Fingerprint", "DNS Resolve Error", "Unknown");
|
||||
|
||||
MQTTClientComponent::MQTTClientComponent() {
|
||||
global_mqtt_client = this;
|
||||
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
|
||||
@@ -348,36 +354,8 @@ void MQTTClientComponent::loop() {
|
||||
mqtt_backend_.loop();
|
||||
|
||||
if (this->disconnect_reason_.has_value()) {
|
||||
const LogString *reason_s;
|
||||
switch (*this->disconnect_reason_) {
|
||||
case MQTTClientDisconnectReason::TCP_DISCONNECTED:
|
||||
reason_s = LOG_STR("TCP disconnected");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
reason_s = LOG_STR("Unacceptable Protocol Version");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED:
|
||||
reason_s = LOG_STR("Identifier Rejected");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE:
|
||||
reason_s = LOG_STR("Server Unavailable");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS:
|
||||
reason_s = LOG_STR("Malformed Credentials");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED:
|
||||
reason_s = LOG_STR("Not Authorized");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE:
|
||||
reason_s = LOG_STR("Not Enough Space");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT:
|
||||
reason_s = LOG_STR("TLS Bad Fingerprint");
|
||||
break;
|
||||
default:
|
||||
reason_s = LOG_STR("Unknown");
|
||||
break;
|
||||
}
|
||||
const LogString *reason_s = MQTTDisconnectReasonStrings::get_log_str(
|
||||
static_cast<uint8_t>(*this->disconnect_reason_), MQTTDisconnectReasonStrings::LAST_INDEX);
|
||||
if (!network::is_connected()) {
|
||||
reason_s = LOG_STR("WiFi disconnected");
|
||||
}
|
||||
|
||||
@@ -13,109 +13,44 @@ static const char *const TAG = "mqtt.climate";
|
||||
|
||||
using namespace esphome::climate;
|
||||
|
||||
// Climate mode MQTT strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO
|
||||
PROGMEM_STRING_TABLE(ClimateMqttModeStrings, "off", "heat_cool", "cool", "heat", "fan_only", "dry", "auto", "unknown");
|
||||
|
||||
static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) {
|
||||
switch (mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_MODE_HEAT_COOL:
|
||||
return ESPHOME_F("heat_cool");
|
||||
case CLIMATE_MODE_AUTO:
|
||||
return ESPHOME_F("auto");
|
||||
case CLIMATE_MODE_COOL:
|
||||
return ESPHOME_F("cool");
|
||||
case CLIMATE_MODE_HEAT:
|
||||
return ESPHOME_F("heat");
|
||||
case CLIMATE_MODE_FAN_ONLY:
|
||||
return ESPHOME_F("fan_only");
|
||||
case CLIMATE_MODE_DRY:
|
||||
return ESPHOME_F("dry");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), ClimateMqttModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate action MQTT strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN
|
||||
PROGMEM_STRING_TABLE(ClimateMqttActionStrings, "off", "unknown", "cooling", "heating", "idle", "drying", "fan",
|
||||
"unknown");
|
||||
|
||||
static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) {
|
||||
switch (action) {
|
||||
case CLIMATE_ACTION_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_ACTION_COOLING:
|
||||
return ESPHOME_F("cooling");
|
||||
case CLIMATE_ACTION_HEATING:
|
||||
return ESPHOME_F("heating");
|
||||
case CLIMATE_ACTION_IDLE:
|
||||
return ESPHOME_F("idle");
|
||||
case CLIMATE_ACTION_DRYING:
|
||||
return ESPHOME_F("drying");
|
||||
case CLIMATE_ACTION_FAN:
|
||||
return ESPHOME_F("fan");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttActionStrings::get_progmem_str(static_cast<uint8_t>(action), ClimateMqttActionStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate fan mode MQTT strings indexed by ClimateFanMode enum (0-9)
|
||||
PROGMEM_STRING_TABLE(ClimateMqttFanModeStrings, "on", "off", "auto", "low", "medium", "high", "middle", "focus",
|
||||
"diffuse", "quiet", "unknown");
|
||||
|
||||
static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) {
|
||||
switch (fan_mode) {
|
||||
case CLIMATE_FAN_ON:
|
||||
return ESPHOME_F("on");
|
||||
case CLIMATE_FAN_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_FAN_AUTO:
|
||||
return ESPHOME_F("auto");
|
||||
case CLIMATE_FAN_LOW:
|
||||
return ESPHOME_F("low");
|
||||
case CLIMATE_FAN_MEDIUM:
|
||||
return ESPHOME_F("medium");
|
||||
case CLIMATE_FAN_HIGH:
|
||||
return ESPHOME_F("high");
|
||||
case CLIMATE_FAN_MIDDLE:
|
||||
return ESPHOME_F("middle");
|
||||
case CLIMATE_FAN_FOCUS:
|
||||
return ESPHOME_F("focus");
|
||||
case CLIMATE_FAN_DIFFUSE:
|
||||
return ESPHOME_F("diffuse");
|
||||
case CLIMATE_FAN_QUIET:
|
||||
return ESPHOME_F("quiet");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttFanModeStrings::get_progmem_str(static_cast<uint8_t>(fan_mode),
|
||||
ClimateMqttFanModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate swing mode MQTT strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL
|
||||
PROGMEM_STRING_TABLE(ClimateMqttSwingModeStrings, "off", "both", "vertical", "horizontal", "unknown");
|
||||
|
||||
static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) {
|
||||
switch (swing_mode) {
|
||||
case CLIMATE_SWING_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_SWING_BOTH:
|
||||
return ESPHOME_F("both");
|
||||
case CLIMATE_SWING_VERTICAL:
|
||||
return ESPHOME_F("vertical");
|
||||
case CLIMATE_SWING_HORIZONTAL:
|
||||
return ESPHOME_F("horizontal");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttSwingModeStrings::get_progmem_str(static_cast<uint8_t>(swing_mode),
|
||||
ClimateMqttSwingModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate preset MQTT strings indexed by ClimatePreset enum (0-7)
|
||||
PROGMEM_STRING_TABLE(ClimateMqttPresetStrings, "none", "home", "away", "boost", "comfort", "eco", "sleep", "activity",
|
||||
"unknown");
|
||||
|
||||
static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) {
|
||||
switch (preset) {
|
||||
case CLIMATE_PRESET_NONE:
|
||||
return ESPHOME_F("none");
|
||||
case CLIMATE_PRESET_HOME:
|
||||
return ESPHOME_F("home");
|
||||
case CLIMATE_PRESET_ECO:
|
||||
return ESPHOME_F("eco");
|
||||
case CLIMATE_PRESET_AWAY:
|
||||
return ESPHOME_F("away");
|
||||
case CLIMATE_PRESET_BOOST:
|
||||
return ESPHOME_F("boost");
|
||||
case CLIMATE_PRESET_COMFORT:
|
||||
return ESPHOME_F("comfort");
|
||||
case CLIMATE_PRESET_SLEEP:
|
||||
return ESPHOME_F("sleep");
|
||||
case CLIMATE_PRESET_ACTIVITY:
|
||||
return ESPHOME_F("activity");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttPresetStrings::get_progmem_str(static_cast<uint8_t>(preset), ClimateMqttPresetStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace esphome::mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt.component";
|
||||
|
||||
// Entity category MQTT strings indexed by EntityCategory enum: NONE(0) is skipped, CONFIG(1), DIAGNOSTIC(2)
|
||||
PROGMEM_STRING_TABLE(EntityCategoryMqttStrings, "", "config", "diagnostic");
|
||||
|
||||
// Helper functions for building topic strings on stack
|
||||
inline char *append_str(char *p, const char *s, size_t len) {
|
||||
memcpy(p, s, len);
|
||||
@@ -213,13 +216,9 @@ bool MQTTComponent::send_discovery_() {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
const auto entity_category = this->get_entity()->get_entity_category();
|
||||
switch (entity_category) {
|
||||
case ENTITY_CATEGORY_NONE:
|
||||
break;
|
||||
case ENTITY_CATEGORY_CONFIG:
|
||||
case ENTITY_CATEGORY_DIAGNOSTIC:
|
||||
root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
|
||||
break;
|
||||
if (entity_category != ENTITY_CATEGORY_NONE) {
|
||||
root[MQTT_ENTITY_CATEGORY] = EntityCategoryMqttStrings::get_progmem_str(
|
||||
static_cast<uint8_t>(entity_category), static_cast<uint8_t>(ENTITY_CATEGORY_CONFIG));
|
||||
}
|
||||
|
||||
if (config.state_topic) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "mqtt_number.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.number";
|
||||
|
||||
using namespace esphome::number;
|
||||
|
||||
// Number mode MQTT strings indexed by NumberMode enum: AUTO(0) is skipped, BOX(1), SLIDER(2)
|
||||
PROGMEM_STRING_TABLE(NumberMqttModeStrings, "", "box", "slider");
|
||||
|
||||
MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {}
|
||||
|
||||
void MQTTNumberComponent::setup() {
|
||||
@@ -48,15 +52,10 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
if (!unit_of_measurement.empty()) {
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
|
||||
}
|
||||
switch (this->number_->traits.get_mode()) {
|
||||
case NUMBER_MODE_AUTO:
|
||||
break;
|
||||
case NUMBER_MODE_BOX:
|
||||
root[MQTT_MODE] = "box";
|
||||
break;
|
||||
case NUMBER_MODE_SLIDER:
|
||||
root[MQTT_MODE] = "slider";
|
||||
break;
|
||||
const auto mode = this->number_->traits.get_mode();
|
||||
if (mode != NUMBER_MODE_AUTO) {
|
||||
root[MQTT_MODE] =
|
||||
NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX));
|
||||
}
|
||||
const auto device_class = this->number_->traits.get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "mqtt_text.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.text";
|
||||
|
||||
using namespace esphome::text;
|
||||
|
||||
// Text mode MQTT strings indexed by TextMode enum (0-1): TEXT, PASSWORD
|
||||
PROGMEM_STRING_TABLE(TextMqttModeStrings, "text", "password");
|
||||
|
||||
MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {}
|
||||
|
||||
void MQTTTextComponent::setup() {
|
||||
@@ -34,14 +38,8 @@ const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
||||
|
||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
switch (this->text_->traits.get_mode()) {
|
||||
case TEXT_MODE_TEXT:
|
||||
root[MQTT_MODE] = "text";
|
||||
break;
|
||||
case TEXT_MODE_PASSWORD:
|
||||
root[MQTT_MODE] = "password";
|
||||
break;
|
||||
}
|
||||
root[MQTT_MODE] = TextMqttModeStrings::get_progmem_str(static_cast<uint8_t>(this->text_->traits.get_mode()),
|
||||
static_cast<uint8_t>(TEXT_MODE_TEXT));
|
||||
|
||||
config.command_topic = true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::template_ {
|
||||
|
||||
@@ -28,18 +29,11 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
|
||||
this->sensor_data_.push_back(sd);
|
||||
};
|
||||
|
||||
// Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS
|
||||
PROGMEM_STRING_TABLE(AlarmSensorTypeStrings, "delayed", "instant", "delayed_follower", "instant_always");
|
||||
|
||||
static const LogString *sensor_type_to_string(AlarmSensorType type) {
|
||||
switch (type) {
|
||||
case ALARM_SENSOR_TYPE_INSTANT:
|
||||
return LOG_STR("instant");
|
||||
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
|
||||
return LOG_STR("delayed_follower");
|
||||
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
|
||||
return LOG_STR("instant_always");
|
||||
case ALARM_SENSOR_TYPE_DELAYED:
|
||||
default:
|
||||
return LOG_STR("delayed");
|
||||
}
|
||||
return AlarmSensorTypeStrings::get_log_str(static_cast<uint8_t>(type), 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ enum BinarySensorFlags : uint16_t {
|
||||
BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4,
|
||||
};
|
||||
|
||||
enum AlarmSensorType : uint16_t {
|
||||
enum AlarmSensorType : uint8_t {
|
||||
ALARM_SENSOR_TYPE_DELAYED = 0,
|
||||
ALARM_SENSOR_TYPE_INSTANT,
|
||||
ALARM_SENSOR_TYPE_DELAYED_FOLLOWER,
|
||||
|
||||
@@ -46,6 +46,7 @@ CONFIG_SCHEMA = (
|
||||
RESTORE_MODES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda,
|
||||
cv.Optional(CONF_MODE): cv.returning_lambda,
|
||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||
water_heater.validate_water_heater_mode
|
||||
@@ -78,6 +79,14 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
cg.add(var.set_current_temperature_lambda(template_))
|
||||
|
||||
if CONF_TARGET_TEMPERATURE in config:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_TARGET_TEMPERATURE],
|
||||
[],
|
||||
return_type=cg.optional.template(cg.float_),
|
||||
)
|
||||
cg.add(var.set_target_temperature_lambda(template_))
|
||||
|
||||
if CONF_MODE in config:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_MODE],
|
||||
|
||||
@@ -16,7 +16,8 @@ void TemplateWaterHeater::setup() {
|
||||
restore->perform();
|
||||
}
|
||||
}
|
||||
if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value())
|
||||
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
|
||||
!this->mode_f_.has_value())
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
@@ -28,6 +29,9 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
|
||||
}
|
||||
|
||||
traits.set_supports_current_temperature(true);
|
||||
if (this->target_temperature_f_.has_value()) {
|
||||
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
|
||||
}
|
||||
return traits;
|
||||
}
|
||||
|
||||
@@ -42,6 +46,14 @@ void TemplateWaterHeater::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
auto target_temp = this->target_temperature_f_.call();
|
||||
if (target_temp.has_value()) {
|
||||
if (*target_temp != this->target_temperature_) {
|
||||
this->target_temperature_ = *target_temp;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto new_mode = this->mode_f_.call();
|
||||
if (new_mode.has_value()) {
|
||||
if (*new_mode != this->mode_) {
|
||||
|
||||
@@ -20,6 +20,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
||||
template<typename F> void set_current_temperature_lambda(F &&f) {
|
||||
this->current_temperature_f_.set(std::forward<F>(f));
|
||||
}
|
||||
template<typename F> void set_target_temperature_lambda(F &&f) {
|
||||
this->target_temperature_f_.set(std::forward<F>(f));
|
||||
}
|
||||
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
|
||||
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
@@ -44,6 +47,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
||||
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
|
||||
Trigger<> set_trigger_;
|
||||
TemplateLambda<float> current_temperature_f_;
|
||||
TemplateLambda<float> target_temperature_f_;
|
||||
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
|
||||
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
||||
water_heater::WaterHeaterModeMask supported_modes_;
|
||||
|
||||
@@ -37,7 +37,7 @@ lib_deps_base =
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||
esphome/dsmr_parser@1.0.0 ; dsmr
|
||||
esphome/dsmr_parser@1.1.0 ; dsmr
|
||||
polargoose/Crypto-no-arduino@0.4.0 ; dsmr
|
||||
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
|
||||
; This is using the repository until a new release is published to PlatformIO
|
||||
|
||||
@@ -23,6 +23,7 @@ resvg-py==0.2.6
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.1
|
||||
requests==2.32.5
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.0
|
||||
|
||||
@@ -412,6 +412,7 @@ water_heater:
|
||||
name: "Template Water Heater"
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 42.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
||||
supported_modes:
|
||||
- "OFF"
|
||||
|
||||
@@ -10,6 +10,7 @@ water_heater:
|
||||
name: Test Boiler
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 45.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
# Note: No mode lambda - we want optimistic mode changes to stick
|
||||
# A mode lambda would override mode changes in loop()
|
||||
supported_modes:
|
||||
|
||||
@@ -85,6 +85,9 @@ async def test_water_heater_template(
|
||||
assert initial_state.current_temperature == 45.0, (
|
||||
f"Expected current temp 45.0, got {initial_state.current_temperature}"
|
||||
)
|
||||
assert initial_state.target_temperature == 60.0, (
|
||||
f"Expected target temp 60.0, got {initial_state.target_temperature}"
|
||||
)
|
||||
|
||||
# Test changing to GAS mode
|
||||
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)
|
||||
|
||||
Reference in New Issue
Block a user