Merge branch 'dev' into mqtt_stack_part_2

This commit is contained in:
J. Nick Koston
2026-01-21 18:37:55 -10:00
committed by GitHub
53 changed files with 362 additions and 224 deletions

View File

@@ -1 +1 @@
c0335c9688ce9defb4a7d4446b93460547e22df055668bacd7d963c770f0c65f
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce

View File

@@ -67,52 +67,29 @@ void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback)
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_away(optional<std::string> code) {
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
const char *code) {
auto call = this->make_call();
call.arm_away();
if (code.has_value())
call.set_code(code.value());
(call.*arm_method)();
if (code != nullptr)
call.set_code(code);
call.perform();
}
void AlarmControlPanel::arm_home(optional<std::string> code) {
auto call = this->make_call();
call.arm_home();
if (code.has_value())
call.set_code(code.value());
call.perform();
void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); }
void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); }
void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); }
void AlarmControlPanel::arm_vacation(const char *code) {
this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code);
}
void AlarmControlPanel::arm_night(optional<std::string> code) {
auto call = this->make_call();
call.arm_night();
if (code.has_value())
call.set_code(code.value());
call.perform();
void AlarmControlPanel::arm_custom_bypass(const char *code) {
this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code);
}
void AlarmControlPanel::arm_vacation(optional<std::string> code) {
auto call = this->make_call();
call.arm_vacation();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
auto call = this->make_call();
call.arm_custom_bypass();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(optional<std::string> code) {
auto call = this->make_call();
call.disarm();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); }
} // namespace esphome::alarm_control_panel

View File

@@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase {
*
* @param code The code
*/
void arm_away(optional<std::string> code = nullopt);
void arm_away(const char *code = nullptr);
void arm_away(const optional<std::string> &code) {
this->arm_away(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in home mode
*
* @param code The code
*/
void arm_home(optional<std::string> code = nullopt);
void arm_home(const char *code = nullptr);
void arm_home(const optional<std::string> &code) {
this->arm_home(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in night mode
*
* @param code The code
*/
void arm_night(optional<std::string> code = nullopt);
void arm_night(const char *code = nullptr);
void arm_night(const optional<std::string> &code) {
this->arm_night(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in vacation mode
*
* @param code The code
*/
void arm_vacation(optional<std::string> code = nullopt);
void arm_vacation(const char *code = nullptr);
void arm_vacation(const optional<std::string> &code) {
this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in custom bypass mode
*
* @param code The code
*/
void arm_custom_bypass(optional<std::string> code = nullopt);
void arm_custom_bypass(const char *code = nullptr);
void arm_custom_bypass(const optional<std::string> &code) {
this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr);
}
/** disarm the alarm
*
* @param code The code
*/
void disarm(optional<std::string> code = nullopt);
void disarm(const char *code = nullptr);
void disarm(const optional<std::string> &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); }
/** Get the state
*
@@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase {
protected:
friend AlarmControlPanelCall;
// Helper to reduce code duplication for arm/disarm methods
void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code);
// in order to store last panel state in flash
ESPPreferenceObject pref_;
// current state

View File

@@ -10,8 +10,10 @@ static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) {
this->code_ = code;
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
if (code != nullptr) {
this->code_ = std::string(code);
}
return *this;
}

View File

@@ -14,7 +14,8 @@ class AlarmControlPanelCall {
public:
AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const std::string &code);
AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night();

View File

@@ -66,15 +66,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_away();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
@@ -86,15 +78,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_home();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
@@ -106,15 +90,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_night();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;

View File

@@ -190,7 +190,7 @@ async def to_code(config):
# Rotation is handled by setting the transform
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
await display.register_display(var, display_config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -182,6 +182,12 @@ def set_core_data(config):
path=[CONF_CPU_FREQUENCY],
)
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
"400MHz on ESP32-P4 is experimental and may not boot. "
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
)
CORE.data[KEY_ESP32] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
conf = config[CONF_FRAMEWORK]
@@ -342,7 +348,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool:
def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
# a PIO pioarduino/framework-arduinoespressif32 value
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
# 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz
if ver >= cv.Version(3, 3, 6):
filename = f"esp32-core-{ver}.tar.xz"
else:
filename = f"esp32-{ver}.zip"
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
@@ -377,11 +388,12 @@ def _is_framework_url(source: str) -> bool:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 5),
"latest": cv.Version(3, 3, 5),
"dev": cv.Version(3, 3, 5),
"recommended": cv.Version(3, 3, 6),
"latest": cv.Version(3, 3, 6),
"dev": cv.Version(3, 3, 6),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
@@ -399,6 +411,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
cv.Version(3, 3, 3): cv.Version(5, 5, 1),
@@ -421,7 +434,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 2),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 2): cv.Version(55, 3, 35),
cv.Version(5, 5, 2): cv.Version(55, 3, 36),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
@@ -438,9 +451,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 35),
"latest": cv.Version(55, 3, 35),
"dev": cv.Version(55, 3, 35),
"recommended": cv.Version(55, 3, 36),
"latest": cv.Version(55, 3, 36),
"dev": cv.Version(55, 3, 36),
}

View File

@@ -12,7 +12,6 @@ extern "C" {
#include "preferences.h"
#include <cstring>
#include <memory>
namespace esphome::esp8266 {
@@ -143,16 +142,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
uint32_t stack_buffer[PREF_BUFFER_WORDS];
std::unique_ptr<uint32_t[]> heap_buffer;
uint32_t *buffer;
if (buffer_size <= PREF_BUFFER_WORDS) {
buffer = stack_buffer;
} else {
heap_buffer = make_unique<uint32_t[]>(buffer_size);
buffer = heap_buffer.get();
}
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
@@ -167,16 +158,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
uint32_t stack_buffer[PREF_BUFFER_WORDS];
std::unique_ptr<uint32_t[]> heap_buffer;
uint32_t *buffer;
if (buffer_size <= PREF_BUFFER_WORDS) {
buffer = stack_buffer;
} else {
heap_buffer = make_unique<uint32_t[]>(buffer_size);
buffer = heap_buffer.get();
}
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size);

View File

@@ -223,7 +223,7 @@ async def to_code(config):
var = cg.Pvariable(config[CONF_ID], rhs)
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))
if init_sequences := config.get(CONF_INIT_SEQUENCE):

View File

@@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
await display.register_display(var, config)
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))

View File

@@ -86,7 +86,7 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
await display.register_display(var, config)
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))

View File

@@ -260,7 +260,7 @@ async def to_code(config):
cg.add(var.set_enable_pins(enable))
if CONF_SPI_ID in config:
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
sequence, madctl = model.get_sequence(config)
cg.add(var.set_init_sequence(sequence))
cg.add(var.set_madctl(madctl))

View File

@@ -443,6 +443,4 @@ async def to_code(config):
)
cg.add(var.set_writer(lambda_))
await display.register_display(var, config)
await spi.register_spi_device(var, config)
# Displays are write-only, set the SPI device to write-only as well
cg.add(var.set_write_only(True))
await spi.register_spi_device(var, config, write_only=True)

View File

@@ -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"

View File

@@ -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) {

View File

@@ -27,20 +27,23 @@ 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 and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h.
// MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are 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; }
@@ -69,19 +72,18 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
return std::string(buf, p - buf);
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
size_t suffix_len) const {
const std::string &topic_prefix = global_mqtt_client->get_topic_prefix();
if (topic_prefix.empty()) {
// If the topic_prefix is null, the default topic should be null
return "";
return StringRef(); // Empty topic_prefix means no default topic
}
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 buf[DEFAULT_TOPIC_MAX_LEN];
char *p = buf;
char *p = buf.data();
p = append_str(p, topic_prefix.data(), topic_prefix.size());
p = append_char(p, '/');
@@ -89,21 +91,44 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_char(p, '/');
p = append_str(p, suffix.data(), suffix.size());
p = append_str(p, suffix, suffix_len);
*p = '\0';
return std::string(buf, p - buf);
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);
}
std::string MQTTComponent::get_state_topic_() const {
if (this->custom_state_topic_.has_value())
return this->custom_state_topic_.value();
return this->get_default_topic_for_("state");
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_state_topic_to_(buf);
return std::string(ref.c_str(), ref.size());
}
std::string MQTTComponent::get_command_topic_() const {
if (this->custom_command_topic_.has_value())
return this->custom_command_topic_.value();
return this->get_default_topic_for_("command");
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_command_topic_to_(buf);
return std::string(ref.c_str(), ref.size());
}
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
@@ -180,10 +205,14 @@ bool MQTTComponent::send_discovery_() {
break;
}
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 (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 (this->command_retain_)
root[MQTT_COMMAND_RETAIN] = true;
@@ -321,7 +350,9 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
}
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
void MQTTComponent::call_setup() {
if (this->is_internal())
// Cache is_internal result once during setup - topics don't change after this
this->is_internal_ = this->compute_is_internal_();
if (this->is_internal_)
return;
this->setup();
@@ -373,26 +404,28 @@ 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::is_internal() {
bool MQTTComponent::compute_is_internal_() {
if (this->custom_state_topic_.has_value()) {
// If the custom state_topic is null, return true as it is internal and should not publish
// If the custom state_topic is empty, 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
return this->get_state_topic_().empty();
// Using is_empty() avoids heap allocation for non-lambda cases
return this->custom_state_topic_.is_empty();
}
if (this->custom_command_topic_.has_value()) {
// If the custom command_topic is null, return true as it is internal and should not publish
// If the custom command_topic is empty, 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
return this->get_command_topic_().empty();
// Using is_empty() avoids heap allocation for non-lambda cases
return this->custom_command_topic_.is_empty();
}
// 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
// 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
return true;
}
// Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic
// Use ESPHome's component internal state if topic_prefix is not empty with no custom state_topic or command_topic
return this->get_entity()->is_internal();
}

View File

@@ -20,17 +20,22 @@ struct SendDiscoveryConfig {
bool command_topic{true}; ///< If the command topic should be included. Default to true.
};
// Max lengths for stack-based topic building (must match mqtt_component.cpp)
// 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.
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;
#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()); \
}
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)
// Macro to define component_type() with compile-time length verification
// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
@@ -74,6 +79,8 @@ static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
* 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();
@@ -88,7 +95,8 @@ class MQTTComponent : public Component {
virtual bool send_initial_state() = 0;
virtual bool is_internal();
/// Returns cached is_internal result (computed once during setup).
bool is_internal() const { return this->is_internal_; }
/// Set QOS for state messages.
void set_qos(uint8_t qos);
@@ -225,7 +233,16 @@ 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.
/** 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).
*
* @param suffix The suffix/key such as "state" or "command".
* @return The full topic.
@@ -246,10 +263,20 @@ class MQTTComponent : public Component {
/// Get whether the underlying Entity is disabled by default
bool is_disabled_by_default_() const;
/// Get the MQTT topic that new states will be shared to.
/// 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).
std::string get_state_topic_() const;
/// Get the MQTT topic for listening to commands.
/// Get the MQTT topic for listening to commands (allocates std::string).
std::string get_command_topic_() const;
bool is_connected_() const;
@@ -267,12 +294,18 @@ class MQTTComponent : public Component {
std::unique_ptr<Availability> availability_;
bool command_retain_{false};
bool retain_{true};
uint8_t qos_{0};
uint8_t subscribe_qos_{0};
bool discovery_enabled_{true};
bool resend_state_{false};
// 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_();
};
} // namespace esphome::mqtt

View File

@@ -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"

View File

@@ -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")

View File

@@ -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")

View File

@@ -91,7 +91,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

View File

@@ -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")

View File

@@ -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")

View File

@@ -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")

View File

@@ -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")

View File

@@ -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")

View File

@@ -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"

View File

@@ -16,6 +16,7 @@ CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
CONF_FONT_ID = "font_id"
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
CONF_MAX_COMMANDS_PER_LOOP = "max_commands_per_loop"
CONF_MAX_QUEUE_AGE = "max_queue_age"
CONF_MAX_QUEUE_SIZE = "max_queue_size"
CONF_ON_BUFFER_OVERFLOW = "on_buffer_overflow"
CONF_ON_PAGE = "on_page"
@@ -25,6 +26,7 @@ CONF_ON_WAKE = "on_wake"
CONF_PRECISION = "precision"
CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake"
CONF_START_UP_PAGE = "start_up_page"
CONF_STARTUP_OVERRIDE_MS = "startup_override_ms"
CONF_TFT_URL = "tft_url"
CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout"
CONF_VARIABLE_NAME = "variable_name"

View File

@@ -23,6 +23,7 @@ from .base_component import (
CONF_DUMP_DEVICE_INFO,
CONF_EXIT_REPARSE_ON_START,
CONF_MAX_COMMANDS_PER_LOOP,
CONF_MAX_QUEUE_AGE,
CONF_MAX_QUEUE_SIZE,
CONF_ON_BUFFER_OVERFLOW,
CONF_ON_PAGE,
@@ -31,6 +32,7 @@ from .base_component import (
CONF_ON_WAKE,
CONF_SKIP_CONNECTION_HANDSHAKE,
CONF_START_UP_PAGE,
CONF_STARTUP_OVERRIDE_MS,
CONF_TFT_URL,
CONF_TOUCH_SLEEP_TIMEOUT,
CONF_WAKE_UP_PAGE,
@@ -65,6 +67,12 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean,
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
cv.Optional(CONF_MAX_QUEUE_AGE, default="8000ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=TimePeriod(milliseconds=0), max=TimePeriod(milliseconds=65535)
),
),
cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t,
cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int,
cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation(
@@ -100,6 +108,12 @@ CONFIG_SCHEMA = (
}
),
cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean,
cv.Optional(CONF_STARTUP_OVERRIDE_MS, default="8000ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=TimePeriod(milliseconds=0), max=TimePeriod(milliseconds=65535)
),
),
cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
cv.Optional(CONF_TFT_URL): cv.url,
cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any(
@@ -138,6 +152,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await uart.register_uart_device(var, config)
cg.add(var.set_max_queue_age(config[CONF_MAX_QUEUE_AGE]))
if max_queue_size := config.get(CONF_MAX_QUEUE_SIZE):
cg.add_define("USE_NEXTION_MAX_QUEUE_SIZE")
cg.add(var.set_max_queue_size(max_queue_size))
@@ -146,6 +162,8 @@ async def to_code(config):
cg.add_define("USE_NEXTION_COMMAND_SPACING")
cg.add(var.set_command_spacing(command_spacing.total_milliseconds))
cg.add(var.set_startup_override_ms(config[CONF_STARTUP_OVERRIDE_MS]))
if CONF_BRIGHTNESS in config:
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))

View File

@@ -152,21 +152,25 @@ void Nextion::dump_config() {
#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
ESP_LOGCONFIG(TAG,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s\n"
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s\n"
" Max queue age: %u ms\n"
" Startup override: %u ms\n",
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Exit reparse: YES\n"
" Exit reparse: YES\n"
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Wake On Touch: %s\n"
" Touch Timeout: %" PRIu16,
" Wake On Touch: %s\n"
" Touch Timeout: %" PRIu16,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
this->flash_size_.c_str(),
this->flash_size_.c_str(), this->max_q_age_ms_,
this->startup_override_ms_
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_);
YESNO(this->connection_state_.auto_wake_on_touch_),
this->touch_sleep_timeout_);
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
@@ -174,21 +178,21 @@ void Nextion::dump_config() {
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (this->wake_up_page_ != 255) {
ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_);
ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_);
}
#ifdef USE_NEXTION_CONF_START_UP_PAGE
if (this->start_up_page_ != 255) {
ESP_LOGCONFIG(TAG, " Start Up Page: %u", this->start_up_page_);
ESP_LOGCONFIG(TAG, " Start Up Page: %u", this->start_up_page_);
}
#endif // USE_NEXTION_CONF_START_UP_PAGE
#ifdef USE_NEXTION_COMMAND_SPACING
ESP_LOGCONFIG(TAG, " Cmd spacing: %u ms", this->command_pacer_.get_spacing());
ESP_LOGCONFIG(TAG, " Cmd spacing: %u ms", this->command_pacer_.get_spacing());
#endif // USE_NEXTION_COMMAND_SPACING
#ifdef USE_NEXTION_MAX_QUEUE_SIZE
ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_);
ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_);
#endif
}
@@ -336,7 +340,8 @@ void Nextion::loop() {
if (this->started_ms_ == 0)
this->started_ms_ = App.get_loop_component_start_time();
if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
if (this->startup_override_ms_ > 0 &&
this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
ESP_LOGV(TAG, "Manual ready set");
this->connection_state_.nextion_reports_is_setup_ = true;
}
@@ -845,7 +850,8 @@ void Nextion::process_nextion_commands_() {
const uint32_t ms = App.get_loop_component_start_time();
if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) {
if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() &&
this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) {
for (size_t i = 0; i < this->nextion_queue_.size(); i++) {
NextionComponentBase *component = this->nextion_queue_[i]->component;
if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) {

View File

@@ -1309,6 +1309,30 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
bool is_connected() { return this->connection_state_.is_connected_; }
/**
* @brief Set the maximum age for queue items
* @param age_ms Maximum age in milliseconds before queue items are removed
*/
inline void set_max_queue_age(uint16_t age_ms) { this->max_q_age_ms_ = age_ms; }
/**
* @brief Get the maximum age for queue items
* @return Maximum age in milliseconds
*/
inline uint16_t get_max_queue_age() const { return this->max_q_age_ms_; }
/**
* @brief Set the startup override timeout
* @param timeout_ms Time in milliseconds to wait before forcing setup complete
*/
inline void set_startup_override_ms(uint16_t timeout_ms) { this->startup_override_ms_ = timeout_ms; }
/**
* @brief Get the startup override timeout
* @return Startup override timeout in milliseconds
*/
inline uint16_t get_startup_override_ms() const { return this->startup_override_ms_; }
protected:
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
uint16_t max_commands_per_loop_{1000};
@@ -1479,9 +1503,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void reset_(bool reset_nextion = true);
std::string command_data_;
const uint16_t startup_override_ms_ = 8000;
const uint16_t max_q_age_ms_ = 8000;
uint32_t started_ms_ = 0;
uint16_t startup_override_ms_ = 8000; ///< Timeout before forcing setup complete
uint16_t max_q_age_ms_ = 8000; ///< Maximum age for queue items in ms
};
} // namespace nextion

View File

@@ -44,7 +44,7 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -161,7 +161,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
chip = DriverChip.chips[config[CONF_MODEL]]
if chip.initsequence:

View File

@@ -39,6 +39,7 @@ from esphome.const import (
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
spi_ns = cg.esphome_ns.namespace("spi")
@@ -448,9 +449,13 @@ def spi_device_schema(
)
async def register_spi_device(var, config):
async def register_spi_device(
var: cg.Pvariable, config: ConfigType, write_only: bool = False
) -> None:
parent = await cg.get_variable(config[CONF_SPI_ID])
cg.add(var.set_spi_parent(parent))
if write_only:
cg.add(var.set_write_only(True))
if cs_pin := config.get(CONF_CS_PIN):
pin = await cg.gpio_pin_expression(cs_pin)
cg.add(var.set_cs_pin(pin))

View File

@@ -195,8 +195,11 @@ class SPIDelegateHw : public SPIDelegate {
config.post_cb = nullptr;
if (this->bit_order_ == BIT_ORDER_LSB_FIRST)
config.flags |= SPI_DEVICE_BIT_LSBFIRST;
if (this->write_only_)
if (this->write_only_) {
config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
ESP_LOGD(TAG, "SPI device with CS pin %d using half-duplex mode (write-only)",
Utility::get_pin_no(this->cs_pin_));
}
esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Add device failed - err %X", err);

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1306_base.setup_ssd1306(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1322_base.setup_ssd1322(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1325_base.setup_ssd1325(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1327_base.setup_ssd1327(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1331_base.setup_ssd1331(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1351_base.setup_ssd1351(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await st7567_base.setup_st7567(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -173,7 +173,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
sequence = []
for seq in config[CONF_INIT_SEQUENCE]:

View File

@@ -99,7 +99,7 @@ async def to_code(config):
config[CONF_INVERT_COLORS],
)
await setup_st7735(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -177,7 +177,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
cg.add(var.set_model_str(config[CONF_MODEL]))

View File

@@ -28,7 +28,7 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(

View File

@@ -239,7 +239,7 @@ async def to_code(config):
raise NotImplementedError()
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -460,13 +460,15 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
}
#endif
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
// For static IP configurations, GOT_IP event may not fire, so set connected state here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
s_sta_state = LTWiFiSTAState::CONNECTED;
#ifdef USE_WIFI_IP_STATE_LISTENERS
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
#endif
}
#endif
break;

View File

@@ -4,6 +4,7 @@
#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>
@@ -190,15 +191,55 @@ 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_; }
protected:
enum : uint8_t {
NONE,
VALUE,
LAMBDA,
STATELESS_LAMBDA,
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
} type_;
/// 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();
}
}
/// 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>;

View File

@@ -234,7 +234,7 @@
#define USB_HOST_MAX_REQUESTS 16
#ifdef USE_ARDUINO
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 5)
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 6)
#define USE_ETHERNET
#define USE_ETHERNET_KSZ8081
#define USE_ETHERNET_MANUAL_IP

View File

@@ -133,9 +133,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
; This are common settings for the ESP32 (all variants) using Arduino.
[common:esp32-arduino]
extends = common:arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.5/esp32-3.3.5.zip
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz
framework = arduino, espidf ; Arduino as an ESP-IDF component
lib_deps =
@@ -168,7 +168,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
; This are common settings for the ESP32 (all variants) using IDF.
[common:esp32-idf]
extends = common:idf
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
platform_packages =
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz

View File

@@ -277,6 +277,8 @@ display:
command_spacing: 5ms
max_commands_per_loop: 20
max_queue_size: 50
startup_override_ms: 10000ms # Wait 10s for display ready
max_queue_age: 5000ms # Remove queue items after 5s
on_sleep:
then:
lambda: 'ESP_LOGD("display","Display went to sleep");'