mirror of
https://github.com/esphome/esphome.git
synced 2026-01-19 17:46:23 -07:00
Compare commits
7 Commits
mqtt_reduc
...
mqtt_less_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcebfe6f48 | ||
|
|
2c10ebe16a | ||
|
|
2970d3d54f | ||
|
|
1996bc425f | ||
|
|
a0d3d54d69 | ||
|
|
ee264d0fd4 | ||
|
|
892e9b006f |
@@ -1,5 +1,6 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import getpass
|
||||
@@ -936,11 +937,21 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
return dashboard.start_dashboard(args)
|
||||
|
||||
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
def run_multiple_configs(
|
||||
files: list, command_builder: Callable[[str], list[str]]
|
||||
) -> int:
|
||||
"""Run a command for each configuration file in a subprocess.
|
||||
|
||||
Args:
|
||||
files: List of configuration files to process.
|
||||
command_builder: Callable that takes a file path and returns a command list.
|
||||
|
||||
Returns:
|
||||
Number of failed files.
|
||||
"""
|
||||
import click
|
||||
|
||||
success = {}
|
||||
files = list_yaml_files(args.configuration)
|
||||
twidth = 60
|
||||
|
||||
def print_bar(middle_text):
|
||||
@@ -950,17 +961,19 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
safe_print(f"{half_line}{middle_text}{half_line}")
|
||||
|
||||
for f in files:
|
||||
safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
|
||||
f_path = Path(f) if not isinstance(f, Path) else f
|
||||
|
||||
if any(f_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", f_path)
|
||||
continue
|
||||
|
||||
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
|
||||
safe_print("-" * twidth)
|
||||
safe_print()
|
||||
if CORE.dashboard:
|
||||
rc = run_external_process(
|
||||
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
|
||||
)
|
||||
else:
|
||||
rc = run_external_process(
|
||||
"esphome", "run", f, "--no-logs", "--device", "OTA"
|
||||
)
|
||||
|
||||
cmd = command_builder(f)
|
||||
rc = run_external_process(*cmd)
|
||||
|
||||
if rc == 0:
|
||||
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
|
||||
success[f] = True
|
||||
@@ -975,6 +988,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
|
||||
failed = 0
|
||||
for f in files:
|
||||
if f not in success:
|
||||
continue # Skipped file
|
||||
if success[f]:
|
||||
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
|
||||
else:
|
||||
@@ -983,6 +998,17 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
return failed
|
||||
|
||||
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
files = list_yaml_files(args.configuration)
|
||||
|
||||
def build_command(f):
|
||||
if CORE.dashboard:
|
||||
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
|
||||
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
|
||||
|
||||
return run_multiple_configs(files, build_command)
|
||||
|
||||
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
@@ -1533,38 +1559,48 @@ def run_esphome(argv):
|
||||
|
||||
_LOGGER.info("ESPHome %s", const.__version__)
|
||||
|
||||
for conf_path in args.configuration:
|
||||
conf_path = Path(conf_path)
|
||||
if any(conf_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
continue
|
||||
# Multiple configurations: use subprocesses to avoid state leakage
|
||||
# between compilations (e.g., LVGL touchscreen state in module globals)
|
||||
if len(args.configuration) > 1:
|
||||
# Build command by reusing argv, replacing all configs with single file
|
||||
# argv[0] is the program path, skip it since we prefix with "esphome"
|
||||
def build_command(f):
|
||||
return (
|
||||
["esphome"]
|
||||
+ [arg for arg in argv[1:] if arg not in args.configuration]
|
||||
+ [str(f)]
|
||||
)
|
||||
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
return run_multiple_configs(args.configuration, build_command)
|
||||
|
||||
# For logs command, skip updating external components
|
||||
skip_external = args.command == "logs"
|
||||
config = read_config(
|
||||
dict(args.substitution) if args.substitution else {},
|
||||
skip_external_update=skip_external,
|
||||
)
|
||||
if config is None:
|
||||
return 2
|
||||
CORE.config = config
|
||||
# Single configuration
|
||||
conf_path = Path(args.configuration[0])
|
||||
if any(conf_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
return 0
|
||||
|
||||
if args.command not in POST_CONFIG_ACTIONS:
|
||||
safe_print(f"Unknown command {args.command}")
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
try:
|
||||
rc = POST_CONFIG_ACTIONS[args.command](args, config)
|
||||
except EsphomeError as e:
|
||||
_LOGGER.error(e, exc_info=args.verbose)
|
||||
return 1
|
||||
if rc != 0:
|
||||
return rc
|
||||
# For logs command, skip updating external components
|
||||
skip_external = args.command == "logs"
|
||||
config = read_config(
|
||||
dict(args.substitution) if args.substitution else {},
|
||||
skip_external_update=skip_external,
|
||||
)
|
||||
if config is None:
|
||||
return 2
|
||||
CORE.config = config
|
||||
|
||||
CORE.reset()
|
||||
return 0
|
||||
if args.command not in POST_CONFIG_ACTIONS:
|
||||
safe_print(f"Unknown command {args.command}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
return POST_CONFIG_ACTIONS[args.command](args, config)
|
||||
except EsphomeError as e:
|
||||
_LOGGER.error(e, exc_info=args.verbose)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -18,31 +18,31 @@ AnovaPacket *AnovaCodec::clean_packet_() {
|
||||
|
||||
AnovaPacket *AnovaCodec::get_read_device_status_request() {
|
||||
this->current_query_ = READ_DEVICE_STATUS;
|
||||
sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DEVICE_STATUS);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
AnovaPacket *AnovaCodec::get_read_target_temp_request() {
|
||||
this->current_query_ = READ_TARGET_TEMPERATURE;
|
||||
sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_TARGET_TEMP);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
AnovaPacket *AnovaCodec::get_read_current_temp_request() {
|
||||
this->current_query_ = READ_CURRENT_TEMPERATURE;
|
||||
sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_CURRENT_TEMP);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
AnovaPacket *AnovaCodec::get_read_unit_request() {
|
||||
this->current_query_ = READ_UNIT;
|
||||
sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_UNIT);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
AnovaPacket *AnovaCodec::get_read_data_request() {
|
||||
this->current_query_ = READ_DATA;
|
||||
sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DATA);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
@@ -50,25 +50,25 @@ AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) {
|
||||
this->current_query_ = SET_TARGET_TEMPERATURE;
|
||||
if (this->fahrenheit_)
|
||||
temperature = ctof(temperature);
|
||||
sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TARGET_TEMP, temperature);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
AnovaPacket *AnovaCodec::get_set_unit_request(char unit) {
|
||||
this->current_query_ = SET_UNIT;
|
||||
sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TEMP_UNIT, unit);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
AnovaPacket *AnovaCodec::get_start_request() {
|
||||
this->current_query_ = START;
|
||||
sprintf((char *) this->packet_.data, CMD_START);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_START);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
AnovaPacket *AnovaCodec::get_stop_request() {
|
||||
this->current_query_ = STOP;
|
||||
sprintf((char *) this->packet_.data, CMD_STOP);
|
||||
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_STOP);
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
|
||||
@@ -1715,7 +1715,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
// HA state max length is 255 characters, but attributes can be much longer
|
||||
// Use stack buffer for common case (states), heap fallback for large attributes
|
||||
size_t state_len = msg.state.size();
|
||||
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1);
|
||||
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
|
||||
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
|
||||
if (state_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), state_len);
|
||||
|
||||
@@ -224,12 +224,9 @@ class MipiSpi : public display::Display,
|
||||
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
|
||||
if (this->brightness_.has_value())
|
||||
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
|
||||
if (this->cs_ != nullptr)
|
||||
esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str());
|
||||
if (this->reset_pin_ != nullptr)
|
||||
esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str());
|
||||
if (this->dc_pin_ != nullptr)
|
||||
esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str());
|
||||
log_pin(TAG, " CS Pin: ", this->cs_);
|
||||
log_pin(TAG, " Reset Pin: ", this->reset_pin_);
|
||||
log_pin(TAG, " DC Pin: ", this->dc_pin_);
|
||||
esph_log_config(TAG,
|
||||
" SPI Mode: %d\n"
|
||||
" SPI Data rate: %dMHz\n"
|
||||
|
||||
@@ -26,5 +26,3 @@ ST7789V.extend(
|
||||
reset_pin=40,
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -105,6 +105,3 @@ CO5300 = DriverChip(
|
||||
(WCE, 0x00),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
from .ili import ILI9341
|
||||
from .ili import ILI9341, ILI9342, ST7789V
|
||||
|
||||
ILI9341.extend(
|
||||
# ESP32-2432S028 CYD board with Micro USB, has ILI9341 controller
|
||||
"ESP32-2432S028",
|
||||
data_rate="40MHz",
|
||||
cs_pin=15,
|
||||
dc_pin=2,
|
||||
cs_pin={"number": 15, "ignore_strapping_warning": True},
|
||||
dc_pin={"number": 2, "ignore_strapping_warning": True},
|
||||
)
|
||||
|
||||
models = {}
|
||||
ST7789V.extend(
|
||||
# ESP32-2432S028 CYD board with USB C + Micro USB, has ST7789V controller
|
||||
"ESP32-2432S028-7789",
|
||||
data_rate="40MHz",
|
||||
cs_pin={"number": 15, "ignore_strapping_warning": True},
|
||||
dc_pin={"number": 2, "ignore_strapping_warning": True},
|
||||
)
|
||||
|
||||
# fmt: off
|
||||
|
||||
ILI9342.extend(
|
||||
# ESP32-2432S028 CYD board with USB C + Micro USB, has ILI9342 controller
|
||||
"ESP32-2432S028-9342",
|
||||
data_rate="40MHz",
|
||||
cs_pin={"number": 15, "ignore_strapping_warning": True},
|
||||
dc_pin={"number": 2, "ignore_strapping_warning": True},
|
||||
initsequence=(
|
||||
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), # Power Control A
|
||||
(0xCF, 0x00, 0xC1, 0x30), # Power Control B
|
||||
(0xE8, 0x85, 0x00, 0x78), # Driver timing control A
|
||||
(0xEA, 0x00, 0x00), # Driver timing control B
|
||||
(0xED, 0x64, 0x03, 0x12, 0x81), # Power on sequence control
|
||||
(0xF7, 0x20), # Pump ratio control
|
||||
(0xC0, 0x23), # Power Control 1
|
||||
(0xC1, 0x10), # Power Control 2
|
||||
(0xC5, 0x3E, 0x28), # VCOM Control 1
|
||||
(0xC7, 0x86), # VCOM Control 2
|
||||
(0xB1, 0x00, 0x1B), # Frame Rate Control
|
||||
(0xB6, 0x0A, 0xA2, 0x27, 0x00), # Display Function Control
|
||||
(0xF2, 0x00), # Enable 3G
|
||||
(0x26, 0x01), # Gamma Set
|
||||
(0xE0, 0x00, 0x0C, 0x11, 0x04, 0x11, 0x08, 0x37, 0x89, 0x4C, 0x06, 0x0C, 0x0A, 0x2E, 0x34, 0x0F), # Positive Gamma Correction
|
||||
(0xE1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x23, 0x33, 0x0F), # Negative Gamma Correction
|
||||
)
|
||||
)
|
||||
|
||||
@@ -148,6 +148,34 @@ ILI9341 = DriverChip(
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# fmt: off
|
||||
|
||||
ILI9342 = DriverChip(
|
||||
"ILI9342",
|
||||
width=320,
|
||||
height=240,
|
||||
mirror_x=True,
|
||||
initsequence=(
|
||||
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), # Power Control A
|
||||
(0xCF, 0x00, 0xC1, 0x30), # Power Control B
|
||||
(0xE8, 0x85, 0x00, 0x78), # Driver timing control A
|
||||
(0xEA, 0x00, 0x00), # Driver timing control B
|
||||
(0xED, 0x64, 0x03, 0x12, 0x81), # Power on sequence control
|
||||
(0xF7, 0x20), # Pump ratio control
|
||||
(0xC0, 0x23), # Power Control 1
|
||||
(0xC1, 0x10), # Power Control 2
|
||||
(0xC5, 0x3E, 0x28), # VCOM Control 1
|
||||
(0xC7, 0x86), # VCOM Control 2
|
||||
(0xB1, 0x00, 0x1B), # Frame Rate Control
|
||||
(0xB6, 0x0A, 0xA2, 0x27, 0x00), # Display Function Control
|
||||
(0xF2, 0x00), # Enable 3G
|
||||
(0x26, 0x01), # Gamma Set
|
||||
(0xE0, 0x0F, 0x1F, 0x1C, 0x0C, 0x0F, 0x08, 0x48, 0x98, 0x37, 0x0A, 0x13, 0x04, 0x11, 0x0D, 0x00), # Positive Gamma
|
||||
(0xE1, 0x0F, 0x32, 0x2E, 0x0B, 0x0D, 0x05, 0x47, 0x75, 0x37, 0x06, 0x10, 0x03, 0x24, 0x20, 0x00), # Negative Gamma
|
||||
),
|
||||
)
|
||||
|
||||
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
|
||||
ILI9341.extend(
|
||||
"M5CORE2",
|
||||
@@ -758,5 +786,3 @@ ST7796.extend(
|
||||
dc_pin=0,
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -588,5 +588,3 @@ DriverChip(
|
||||
(0x29, 0x00),
|
||||
),
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -11,5 +11,3 @@ ST7789V.extend(
|
||||
dc_pin=21,
|
||||
reset_pin=18,
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -56,5 +56,3 @@ ST7796.extend(
|
||||
backlight_pin=48,
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -43,7 +43,7 @@ void MQTTAlarmControlPanelComponent::setup() {
|
||||
|
||||
void MQTTAlarmControlPanelComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Supported Features: %" PRIu32 "\n"
|
||||
" Requires Code to Disarm: %s\n"
|
||||
|
||||
@@ -19,7 +19,7 @@ void MQTTBinarySensorComponent::setup() {
|
||||
|
||||
void MQTTBinarySensorComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
|
||||
: binary_sensor_(binary_sensor) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <utility>
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/version.h"
|
||||
@@ -66,10 +67,13 @@ void MQTTClientComponent::setup() {
|
||||
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
|
||||
2);
|
||||
|
||||
std::string topic = "esphome/ping/";
|
||||
topic.append(App.get_name());
|
||||
// Format topic on stack - subscribe() copies it
|
||||
// "esphome/ping/" (13) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1)
|
||||
constexpr size_t ping_topic_buffer_size = 13 + ESPHOME_DEVICE_NAME_MAX_LEN + 1;
|
||||
char ping_topic[ping_topic_buffer_size];
|
||||
buf_append_printf(ping_topic, sizeof(ping_topic), 0, "esphome/ping/%s", App.get_name().c_str());
|
||||
this->subscribe(
|
||||
topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
|
||||
ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
|
||||
}
|
||||
|
||||
if (this->enable_on_boot_) {
|
||||
@@ -81,8 +85,11 @@ void MQTTClientComponent::send_device_info_() {
|
||||
if (!this->is_connected() or !this->is_discovery_ip_enabled()) {
|
||||
return;
|
||||
}
|
||||
std::string topic = "esphome/discover/";
|
||||
topic.append(App.get_name());
|
||||
// Format topic on stack to avoid heap allocation
|
||||
// "esphome/discover/" (17) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1)
|
||||
constexpr size_t topic_buffer_size = 17 + ESPHOME_DEVICE_NAME_MAX_LEN + 1;
|
||||
char topic[topic_buffer_size];
|
||||
buf_append_printf(topic, sizeof(topic), 0, "esphome/discover/%s", App.get_name().c_str());
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
this->publish_json(
|
||||
@@ -500,39 +507,49 @@ bool MQTTClientComponent::publish(const std::string &topic, const std::string &p
|
||||
|
||||
bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos,
|
||||
bool retain) {
|
||||
return publish({.topic = topic, .payload = std::string(payload, payload_length), .qos = qos, .retain = retain});
|
||||
return this->publish(topic.c_str(), payload, payload_length, qos, retain);
|
||||
}
|
||||
|
||||
bool MQTTClientComponent::publish(const MQTTMessage &message) {
|
||||
return this->publish(message.topic.c_str(), message.payload.c_str(), message.payload.length(), message.qos,
|
||||
message.retain);
|
||||
}
|
||||
bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos,
|
||||
bool retain) {
|
||||
return this->publish_json(topic.c_str(), f, qos, retain);
|
||||
}
|
||||
|
||||
bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t payload_length, uint8_t qos,
|
||||
bool retain) {
|
||||
if (!this->is_connected()) {
|
||||
// critical components will re-transmit their messages
|
||||
return false;
|
||||
}
|
||||
bool logging_topic = this->log_message_.topic == message.topic;
|
||||
bool ret = this->mqtt_backend_.publish(message);
|
||||
size_t topic_len = strlen(topic);
|
||||
bool logging_topic = (topic_len == this->log_message_.topic.size()) &&
|
||||
(memcmp(this->log_message_.topic.c_str(), topic, topic_len) == 0);
|
||||
bool ret = this->mqtt_backend_.publish(topic, payload, payload_length, qos, retain);
|
||||
delay(0);
|
||||
if (!ret && !logging_topic && this->is_connected()) {
|
||||
delay(0);
|
||||
ret = this->mqtt_backend_.publish(message);
|
||||
ret = this->mqtt_backend_.publish(topic, payload, payload_length, qos, retain);
|
||||
delay(0);
|
||||
}
|
||||
|
||||
if (!logging_topic) {
|
||||
if (ret) {
|
||||
ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d qos=%d)", message.topic.c_str(), message.payload.c_str(),
|
||||
message.retain, message.qos);
|
||||
ESP_LOGV(TAG, "Publish(topic='%s' retain=%d qos=%d)", topic, retain, qos);
|
||||
ESP_LOGVV(TAG, "Publish payload (len=%u): '%.*s'", payload_length, static_cast<int>(payload_length), payload);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", message.topic.c_str(),
|
||||
message.payload.length());
|
||||
ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", topic, payload_length);
|
||||
this->status_momentary_warning("publish", 1000);
|
||||
}
|
||||
}
|
||||
return ret != 0;
|
||||
}
|
||||
bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos,
|
||||
bool retain) {
|
||||
|
||||
bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) {
|
||||
std::string message = json::build_json(f);
|
||||
return this->publish(topic, message, qos, retain);
|
||||
return this->publish(topic, message.c_str(), message.length(), qos, retain);
|
||||
}
|
||||
|
||||
void MQTTClientComponent::enable() {
|
||||
|
||||
@@ -229,6 +229,9 @@ class MQTTClientComponent : public Component
|
||||
bool publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos = 0,
|
||||
bool retain = false);
|
||||
|
||||
/// Publish directly without creating MQTTMessage (avoids heap allocation for topic)
|
||||
bool publish(const char *topic, const char *payload, size_t payload_length, uint8_t qos = 0, bool retain = false);
|
||||
|
||||
/** Construct and send a JSON MQTT message.
|
||||
*
|
||||
* @param topic The topic.
|
||||
@@ -237,6 +240,9 @@ class MQTTClientComponent : public Component
|
||||
*/
|
||||
bool publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos = 0, bool retain = false);
|
||||
|
||||
/// Publish JSON directly without heap allocation for topic
|
||||
bool publish_json(const char *topic, const json::json_build_t &f, uint8_t qos = 0, bool retain = false);
|
||||
|
||||
/// Setup the MQTT client, registering a bunch of callbacks and attempting to connect.
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
@@ -27,23 +27,20 @@ inline char *append_char(char *p, char c) {
|
||||
// Max lengths for stack-based topic building.
|
||||
// These limits are enforced at Python config validation time in mqtt/__init__.py
|
||||
// using cv.Length() validators for topic_prefix and discovery_prefix.
|
||||
// MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h.
|
||||
// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h.
|
||||
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
|
||||
// This ensures the stack buffers below are always large enough.
|
||||
static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
|
||||
// Stack buffer sizes - safe because all inputs are length-validated at config time
|
||||
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
|
||||
static constexpr size_t DEFAULT_TOPIC_MAX_LEN =
|
||||
TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
|
||||
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
|
||||
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
|
||||
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
|
||||
|
||||
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (state_topic)
|
||||
ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str());
|
||||
if (command_topic)
|
||||
ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str());
|
||||
}
|
||||
|
||||
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
|
||||
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
|
||||
@@ -72,18 +69,19 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
|
||||
return std::string(buf, p - buf);
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
||||
size_t suffix_len) const {
|
||||
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
|
||||
const std::string &topic_prefix = global_mqtt_client->get_topic_prefix();
|
||||
if (topic_prefix.empty()) {
|
||||
return StringRef(); // Empty topic_prefix means no default topic
|
||||
// If the topic_prefix is null, the default topic should be null
|
||||
return "";
|
||||
}
|
||||
|
||||
const char *comp_type = this->component_type();
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
|
||||
|
||||
char *p = buf.data();
|
||||
char buf[DEFAULT_TOPIC_MAX_LEN];
|
||||
char *p = buf;
|
||||
|
||||
p = append_str(p, topic_prefix.data(), topic_prefix.size());
|
||||
p = append_char(p, '/');
|
||||
@@ -91,44 +89,21 @@ StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_
|
||||
p = append_char(p, '/');
|
||||
p = append_str(p, object_id.c_str(), object_id.size());
|
||||
p = append_char(p, '/');
|
||||
p = append_str(p, suffix, suffix_len);
|
||||
*p = '\0';
|
||||
p = append_str(p, suffix.data(), suffix.size());
|
||||
|
||||
return StringRef(buf.data(), p - buf.data());
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_default_topic_for_to_(buf, suffix.data(), suffix.size());
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
|
||||
if (this->custom_state_topic_.has_value()) {
|
||||
// Returns ref to existing data for static/value, uses buf only for lambda case
|
||||
return this->custom_state_topic_.ref_or_copy_to(buf.data(), buf.size());
|
||||
}
|
||||
return this->get_default_topic_for_to_(buf, "state", 5);
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
|
||||
if (this->custom_command_topic_.has_value()) {
|
||||
// Returns ref to existing data for static/value, uses buf only for lambda case
|
||||
return this->custom_command_topic_.ref_or_copy_to(buf.data(), buf.size());
|
||||
}
|
||||
return this->get_default_topic_for_to_(buf, "command", 7);
|
||||
return std::string(buf, p - buf);
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_state_topic_() const {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_state_topic_to_(buf);
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
if (this->custom_state_topic_.has_value())
|
||||
return this->custom_state_topic_.value();
|
||||
return this->get_default_topic_for_("state");
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_command_topic_() const {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_command_topic_to_(buf);
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
if (this->custom_command_topic_.has_value())
|
||||
return this->custom_command_topic_.value();
|
||||
return this->get_default_topic_for_("command");
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
|
||||
@@ -193,14 +168,10 @@ bool MQTTComponent::send_discovery_() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (config.state_topic) {
|
||||
char state_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
root[MQTT_STATE_TOPIC] = this->get_state_topic_to_(state_topic_buf);
|
||||
}
|
||||
if (config.command_topic) {
|
||||
char command_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_to_(command_topic_buf);
|
||||
}
|
||||
if (config.state_topic)
|
||||
root[MQTT_STATE_TOPIC] = this->get_state_topic_();
|
||||
if (config.command_topic)
|
||||
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_();
|
||||
if (this->command_retain_)
|
||||
root[MQTT_COMMAND_RETAIN] = true;
|
||||
|
||||
@@ -317,9 +288,7 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
|
||||
}
|
||||
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
|
||||
void MQTTComponent::call_setup() {
|
||||
// Cache is_internal result once during setup - topics don't change after this
|
||||
this->is_internal_ = this->compute_is_internal_();
|
||||
if (this->is_internal_)
|
||||
if (this->is_internal())
|
||||
return;
|
||||
|
||||
this->setup();
|
||||
@@ -375,28 +344,26 @@ StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX
|
||||
}
|
||||
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
|
||||
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
|
||||
bool MQTTComponent::compute_is_internal_() {
|
||||
bool MQTTComponent::is_internal() {
|
||||
if (this->custom_state_topic_.has_value()) {
|
||||
// If the custom state_topic is empty, return true as it is internal and should not publish
|
||||
// If the custom state_topic is null, return true as it is internal and should not publish
|
||||
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
|
||||
// Using is_empty() avoids heap allocation for non-lambda cases
|
||||
return this->custom_state_topic_.is_empty();
|
||||
return this->get_state_topic_().empty();
|
||||
}
|
||||
|
||||
if (this->custom_command_topic_.has_value()) {
|
||||
// If the custom command_topic is empty, return true as it is internal and should not publish
|
||||
// If the custom command_topic is null, return true as it is internal and should not publish
|
||||
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
|
||||
// Using is_empty() avoids heap allocation for non-lambda cases
|
||||
return this->custom_command_topic_.is_empty();
|
||||
return this->get_command_topic_().empty();
|
||||
}
|
||||
|
||||
// No custom topics have been set - check topic_prefix directly to avoid allocation
|
||||
if (global_mqtt_client->get_topic_prefix().empty()) {
|
||||
// If the default topic prefix is empty, then the component, by default, is internal and should not publish
|
||||
// No custom topics have been set
|
||||
if (this->get_default_topic_for_("").empty()) {
|
||||
// If the default topic prefix is null, then the component, by default, is internal and should not publish
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use ESPHome's component internal state if topic_prefix is not empty with no custom state_topic or command_topic
|
||||
// Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic
|
||||
return this->get_entity()->is_internal();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,22 +20,17 @@ struct SendDiscoveryConfig {
|
||||
bool command_topic{true}; ///< If the command topic should be included. Default to true.
|
||||
};
|
||||
|
||||
// Max lengths for stack-based topic building.
|
||||
// These limits are enforced at Python config validation time in mqtt/__init__.py
|
||||
// using cv.Length() validators for topic_prefix and discovery_prefix.
|
||||
// This ensures the stack buffers are always large enough.
|
||||
// Max lengths for stack-based topic building (must match mqtt_component.cpp)
|
||||
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
|
||||
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
|
||||
static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
// Stack buffer size - safe because all inputs are length-validated at config time
|
||||
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
|
||||
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
|
||||
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
|
||||
|
||||
class MQTTComponent; // Forward declaration
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
||||
|
||||
#define LOG_MQTT_COMPONENT(state_topic, command_topic) log_mqtt_component(TAG, this, state_topic, command_topic)
|
||||
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \
|
||||
if (state_topic) { \
|
||||
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
|
||||
} \
|
||||
if (command_topic) { \
|
||||
ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \
|
||||
}
|
||||
|
||||
// Macro to define component_type() with compile-time length verification
|
||||
// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
|
||||
@@ -79,8 +74,6 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
|
||||
* a clean separation.
|
||||
*/
|
||||
class MQTTComponent : public Component {
|
||||
friend void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
||||
|
||||
public:
|
||||
/// Constructs a MQTTComponent.
|
||||
explicit MQTTComponent();
|
||||
@@ -97,8 +90,7 @@ class MQTTComponent : public Component {
|
||||
|
||||
virtual bool send_initial_state() = 0;
|
||||
|
||||
/// Returns cached is_internal result (computed once during setup).
|
||||
bool is_internal() const { return this->is_internal_; }
|
||||
virtual bool is_internal();
|
||||
|
||||
/// Set QOS for state messages.
|
||||
void set_qos(uint8_t qos);
|
||||
@@ -186,16 +178,7 @@ class MQTTComponent : public Component {
|
||||
/// Helper method to get the discovery topic for this component.
|
||||
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
|
||||
|
||||
/** Get this components state/command/... topic into a buffer.
|
||||
*
|
||||
* @param buf The buffer to write to (must be exactly MQTT_DEFAULT_TOPIC_MAX_LEN).
|
||||
* @param suffix The suffix/key such as "state" or "command".
|
||||
* @return StringRef pointing to the buffer with the topic.
|
||||
*/
|
||||
StringRef get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
||||
size_t suffix_len) const;
|
||||
|
||||
/** Get this components state/command/... topic (allocates std::string).
|
||||
/** Get this components state/command/... topic.
|
||||
*
|
||||
* @param suffix The suffix/key such as "state" or "command".
|
||||
* @return The full topic.
|
||||
@@ -216,20 +199,10 @@ class MQTTComponent : public Component {
|
||||
/// Get whether the underlying Entity is disabled by default
|
||||
bool is_disabled_by_default_() const;
|
||||
|
||||
/// Get the MQTT state topic into a buffer (no heap allocation for non-lambda custom topics).
|
||||
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
|
||||
/// @return StringRef pointing to the topic in the buffer.
|
||||
StringRef get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
|
||||
|
||||
/// Get the MQTT command topic into a buffer (no heap allocation for non-lambda custom topics).
|
||||
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
|
||||
/// @return StringRef pointing to the topic in the buffer.
|
||||
StringRef get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
|
||||
|
||||
/// Get the MQTT topic that new states will be shared to (allocates std::string).
|
||||
/// Get the MQTT topic that new states will be shared to.
|
||||
std::string get_state_topic_() const;
|
||||
|
||||
/// Get the MQTT topic for listening to commands (allocates std::string).
|
||||
/// Get the MQTT topic for listening to commands.
|
||||
std::string get_command_topic_() const;
|
||||
|
||||
bool is_connected_() const;
|
||||
@@ -247,18 +220,12 @@ class MQTTComponent : public Component {
|
||||
|
||||
std::unique_ptr<Availability> availability_;
|
||||
|
||||
// Packed bitfields - QoS values are 0-2, bools are flags
|
||||
uint8_t qos_ : 2 {0};
|
||||
uint8_t subscribe_qos_ : 2 {0};
|
||||
bool command_retain_ : 1 {false};
|
||||
bool retain_ : 1 {true};
|
||||
bool discovery_enabled_ : 1 {true};
|
||||
bool resend_state_ : 1 {false};
|
||||
bool is_internal_ : 1 {false}; ///< Cached result of compute_is_internal_(), set during setup
|
||||
|
||||
/// Compute is_internal status based on topics and entity state.
|
||||
/// Called once during setup to cache the result.
|
||||
bool compute_is_internal_();
|
||||
bool command_retain_{false};
|
||||
bool retain_{true};
|
||||
uint8_t qos_{0};
|
||||
uint8_t subscribe_qos_{0};
|
||||
bool discovery_enabled_{true};
|
||||
bool resend_state_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -51,7 +51,7 @@ void MQTTCoverComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
|
||||
auto traits = this->cover_->get_traits();
|
||||
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic);
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic)
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Position State Topic: '%s'\n"
|
||||
|
||||
@@ -36,7 +36,7 @@ void MQTTDateComponent::setup() {
|
||||
|
||||
void MQTTDateComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")
|
||||
|
||||
@@ -47,7 +47,7 @@ void MQTTDateTimeComponent::setup() {
|
||||
|
||||
void MQTTDateTimeComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime")
|
||||
|
||||
@@ -90,7 +90,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
||||
bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); }
|
||||
void MQTTJSONLightComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -30,7 +30,7 @@ void MQTTNumberComponent::setup() {
|
||||
|
||||
void MQTTNumberComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number")
|
||||
|
||||
@@ -25,7 +25,7 @@ void MQTTSelectComponent::setup() {
|
||||
|
||||
void MQTTSelectComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select")
|
||||
|
||||
@@ -28,7 +28,7 @@ void MQTTSensorComponent::dump_config() {
|
||||
if (this->get_expire_after() > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000);
|
||||
}
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
|
||||
|
||||
@@ -26,7 +26,7 @@ void MQTTTextComponent::setup() {
|
||||
|
||||
void MQTTTextComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTTextComponent, "text")
|
||||
|
||||
@@ -36,7 +36,7 @@ void MQTTTimeComponent::setup() {
|
||||
|
||||
void MQTTTimeComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time")
|
||||
|
||||
@@ -39,7 +39,7 @@ void MQTTValveComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT valve '%s':", this->valve_->get_name().c_str());
|
||||
auto traits = this->valve_->get_traits();
|
||||
bool has_command_topic = traits.get_supports_position();
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic);
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic)
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Position State Topic: '%s'\n"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include <concepts>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
@@ -191,55 +190,15 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
/// Get the static string pointer (only valid if is_static_string() returns true)
|
||||
const char *get_static_string() const { return this->static_str_; }
|
||||
|
||||
/// Check if the string value is empty without allocating (for std::string specialization).
|
||||
/// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation.
|
||||
/// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate.
|
||||
bool is_empty() const requires std::same_as<T, std::string> {
|
||||
switch (this->type_) {
|
||||
case NONE:
|
||||
return true;
|
||||
case STATIC_STRING:
|
||||
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
|
||||
case VALUE:
|
||||
return this->value_->empty();
|
||||
default: // LAMBDA/STATELESS_LAMBDA - must call value()
|
||||
return this->value().empty();
|
||||
}
|
||||
}
|
||||
protected:
|
||||
enum : uint8_t {
|
||||
NONE,
|
||||
VALUE,
|
||||
LAMBDA,
|
||||
STATELESS_LAMBDA,
|
||||
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
|
||||
} type_;
|
||||
|
||||
/// Get a StringRef to the string value without heap allocation when possible.
|
||||
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
|
||||
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
|
||||
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
|
||||
/// @param lambda_buf_size Size of the buffer.
|
||||
/// @return StringRef pointing to the string data.
|
||||
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
|
||||
switch (this->type_) {
|
||||
case NONE:
|
||||
return StringRef();
|
||||
case STATIC_STRING:
|
||||
if (this->static_str_ == nullptr)
|
||||
return StringRef();
|
||||
return StringRef(this->static_str_, strlen(this->static_str_));
|
||||
case VALUE:
|
||||
return StringRef(this->value_->data(), this->value_->size());
|
||||
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
|
||||
std::string result = this->value();
|
||||
size_t copy_len = std::min(result.size(), lambda_buf_size - 1);
|
||||
memcpy(lambda_buf, result.data(), copy_len);
|
||||
lambda_buf[copy_len] = '\0';
|
||||
return StringRef(lambda_buf, copy_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected : enum : uint8_t {
|
||||
NONE,
|
||||
VALUE,
|
||||
LAMBDA,
|
||||
STATELESS_LAMBDA,
|
||||
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
|
||||
} type_;
|
||||
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
|
||||
// For other types, store value inline as before.
|
||||
using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>;
|
||||
|
||||
@@ -34,6 +34,7 @@ from esphome.__main__ import (
|
||||
has_non_ip_address,
|
||||
has_resolvable_address,
|
||||
mqtt_get_ip,
|
||||
run_esphome,
|
||||
run_miniterm,
|
||||
show_logs,
|
||||
upload_program,
|
||||
@@ -1988,7 +1989,7 @@ esp32:
|
||||
clean_output = strip_ansi_codes(captured.out)
|
||||
|
||||
assert "test-device_123.yaml" in clean_output
|
||||
assert "Updating" in clean_output
|
||||
assert "Processing" in clean_output
|
||||
assert "SUCCESS" in clean_output
|
||||
assert "SUMMARY" in clean_output
|
||||
|
||||
@@ -3172,3 +3173,66 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None:
|
||||
x_count = printed_line.count("X")
|
||||
assert x_count < 150, f"Expected truncation but got {x_count} X's"
|
||||
assert x_count == 95, f"Expected 95 X's after truncation but got {x_count}"
|
||||
|
||||
|
||||
def test_run_esphome_multiple_configs_with_secrets(
|
||||
tmp_path: Path,
|
||||
mock_run_external_process: Mock,
|
||||
capfd: CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test run_esphome with multiple configs and secrets file.
|
||||
|
||||
Verifies:
|
||||
- Multiple configs use subprocess isolation
|
||||
- Secrets files are skipped with warning
|
||||
- Secrets files don't appear in summary
|
||||
"""
|
||||
# Create two config files and a secrets file
|
||||
yaml_file1 = tmp_path / "device1.yaml"
|
||||
yaml_file1.write_text("""
|
||||
esphome:
|
||||
name: device1
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
""")
|
||||
yaml_file2 = tmp_path / "device2.yaml"
|
||||
yaml_file2.write_text("""
|
||||
esphome:
|
||||
name: device2
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
""")
|
||||
secrets_file = tmp_path / "secrets.yaml"
|
||||
secrets_file.write_text("wifi_password: secret123\n")
|
||||
|
||||
setup_core(tmp_path=tmp_path)
|
||||
mock_run_external_process.return_value = 0
|
||||
|
||||
# run_esphome expects argv[0] to be the program name (gets sliced off by parse_args)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
result = run_esphome(
|
||||
["esphome", "compile", str(yaml_file1), str(secrets_file), str(yaml_file2)]
|
||||
)
|
||||
|
||||
assert result == 0
|
||||
|
||||
# Check secrets file was skipped with warning
|
||||
assert "Skipping secrets file" in caplog.text
|
||||
assert "secrets.yaml" in caplog.text
|
||||
|
||||
captured = capfd.readouterr()
|
||||
clean_output = strip_ansi_codes(captured.out)
|
||||
|
||||
# Both config files should be processed
|
||||
assert "device1.yaml" in clean_output
|
||||
assert "device2.yaml" in clean_output
|
||||
assert "SUMMARY" in clean_output
|
||||
|
||||
# Secrets should not appear in summary
|
||||
summary_section = (
|
||||
clean_output.split("SUMMARY")[1] if "SUMMARY" in clean_output else ""
|
||||
)
|
||||
assert "secrets.yaml" not in summary_section
|
||||
|
||||
Reference in New Issue
Block a user