mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d6dc59f62 | ||
|
|
af289c2ba7 | ||
|
|
ff5d9a444c | ||
|
|
a95bf5693b | ||
|
|
c3dc0a19f4 | ||
|
|
1c8eb3eca6 | ||
|
|
56702436c6 | ||
|
|
d8d4c2f121 | ||
|
|
d220f9c395 | ||
|
|
501d9af00c |
@@ -94,10 +94,12 @@ DEFAULT_WS_PATH = "/mqtt"
|
||||
|
||||
# Max lengths for stack-based topic building.
|
||||
# These values are used in cv.Length() validators below to ensure the C++ code
|
||||
# in mqtt_component.cpp can safely use fixed-size stack buffers without overflow.
|
||||
# If you change these, update the corresponding constants in mqtt_component.cpp.
|
||||
# can safely use fixed-size stack buffers without overflow. If you change these,
|
||||
# update the corresponding constants in mqtt_client.h and mqtt_component.h.
|
||||
TOPIC_PREFIX_MAX_LEN = 64 # Default is device name, typically short
|
||||
DISCOVERY_PREFIX_MAX_LEN = 64 # Default is "homeassistant" (13 chars)
|
||||
# Size of the stack buffer topics are built into (static_assert in mqtt_client.cpp)
|
||||
MQTT_DEFAULT_TOPIC_MAX_LEN = 248
|
||||
|
||||
|
||||
def validate_message_just_topic(value):
|
||||
@@ -127,6 +129,15 @@ MQTT_MESSAGE_SCHEMA = cv.Any(
|
||||
)
|
||||
|
||||
mqtt_ns = cg.esphome_ns.namespace("mqtt")
|
||||
|
||||
|
||||
def _topic_source(topic: str) -> cg.MockObj:
|
||||
# On ESP8266 a topic that fits the buffer stays in flash and is copied out when needed
|
||||
if CORE.is_esp8266 and len(topic.encode()) < MQTT_DEFAULT_TOPIC_MAX_LEN:
|
||||
return mqtt_ns.progmem_topic(cg.FlashStringLiteral(topic))
|
||||
return mqtt_ns.static_topic(topic)
|
||||
|
||||
|
||||
MQTTMessage = mqtt_ns.struct("MQTTMessage")
|
||||
MQTTClientDisconnectReason = mqtt_ns.enum("MQTTClientDisconnectReason")
|
||||
MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component)
|
||||
@@ -566,9 +577,7 @@ async def to_code(config):
|
||||
((CONF_PAYLOAD, cg.std_string, automation.string_ref_literal),),
|
||||
),
|
||||
)
|
||||
cg.add(
|
||||
var.subscribe(cg.progmem_string(conf[CONF_TOPIC]), callback, conf[CONF_QOS])
|
||||
)
|
||||
cg.add(var.subscribe(_topic_source(conf[CONF_TOPIC]), callback, conf[CONF_QOS]))
|
||||
|
||||
for conf in config.get(CONF_ON_JSON_MESSAGE, []):
|
||||
callback = await automation.build_trigger_callback(
|
||||
@@ -579,7 +588,7 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(
|
||||
var.subscribe_json(
|
||||
cg.progmem_string(conf[CONF_TOPIC]), callback, conf[CONF_QOS]
|
||||
_topic_source(conf[CONF_TOPIC]), callback, conf[CONF_QOS]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ static bool apply_command(AlarmControlPanelCall &call, const char *state) {
|
||||
void MQTTAlarmControlPanelComponent::setup() {
|
||||
this->alarm_control_panel_->add_on_state_callback(
|
||||
[this](AlarmControlPanelState /*state*/) { this->publish_state(); });
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->alarm_control_panel_->make_call();
|
||||
if (!payload.empty() && payload[0] == '{') {
|
||||
// JSON payload: {"state": "DISARM", "code": "1234"}
|
||||
|
||||
@@ -15,7 +15,7 @@ using namespace esphome::button;
|
||||
MQTTButtonComponent::MQTTButtonComponent(button::Button *button) : button_(button) {}
|
||||
|
||||
void MQTTButtonComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
|
||||
if (payload == "PRESS") {
|
||||
this->button_->press();
|
||||
} else {
|
||||
|
||||
@@ -98,12 +98,16 @@ void MQTTClientComponent::setup() {
|
||||
|
||||
if (this->is_discovery_ip_enabled()) {
|
||||
this->subscribe(
|
||||
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
|
||||
2);
|
||||
static_topic("esphome/discover"),
|
||||
[this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
|
||||
|
||||
// Format topic on stack - subscribe() copies it
|
||||
char ping_topic[ESPHOME_TOPIC_BUFFER_SIZE];
|
||||
format_esphome_topic(ping_topic, "ping");
|
||||
static_assert(sizeof("esphome/ping/") + ESPHOME_DEVICE_NAME_MAX_LEN <= MQTT_DEFAULT_TOPIC_MAX_LEN);
|
||||
MQTTTopicSource ping_topic{[](const void *, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) {
|
||||
size_t len = buf_append_printf(buf.data(), buf.size(), 0, "esphome/ping/%s",
|
||||
App.get_name().c_str());
|
||||
return StringRef(buf.data(), len);
|
||||
},
|
||||
nullptr};
|
||||
this->subscribe(
|
||||
ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
|
||||
}
|
||||
@@ -479,6 +483,12 @@ bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) {
|
||||
}
|
||||
return ret != 0;
|
||||
}
|
||||
// Out of line so the topic buffer is not part of loop()'s stack frame
|
||||
__attribute__((noinline)) bool MQTTClientComponent::subscribe_topic_(const MQTTTopicSource &topic, uint8_t qos) {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->subscribe_(topic.build(buf).c_str(), qos);
|
||||
}
|
||||
|
||||
void MQTTClientComponent::resubscribe_subscription_(MQTTSubscription *sub) {
|
||||
if (sub->subscribed)
|
||||
return;
|
||||
@@ -487,7 +497,7 @@ void MQTTClientComponent::resubscribe_subscription_(MQTTSubscription *sub) {
|
||||
bool do_resub = sub->resubscribe_timeout == 0 || now - sub->resubscribe_timeout > 1000;
|
||||
|
||||
if (do_resub) {
|
||||
sub->subscribed = this->subscribe_(sub->topic.c_str(), sub->qos);
|
||||
sub->subscribed = this->subscribe_topic_(sub->topic, sub->qos);
|
||||
sub->resubscribe_timeout = now;
|
||||
}
|
||||
}
|
||||
@@ -510,16 +520,47 @@ void MQTTClientComponent::handle_crash_log_request_(const std::string &topic, co
|
||||
}
|
||||
#endif
|
||||
|
||||
void MQTTClientComponent::add_subscription_(std::string &&topic, mqtt_subscription_callback_t callback, bool boxed,
|
||||
static StringRef static_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN>) {
|
||||
return StringRef(static_cast<const char *>(context));
|
||||
}
|
||||
// Same as static_topic_builder, but a distinct function so its address marks the topics to free
|
||||
static StringRef copied_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN>) {
|
||||
return StringRef(static_cast<const char *>(context));
|
||||
}
|
||||
|
||||
MQTTTopicSource static_topic(const char *topic) { return {static_topic_builder, topic}; }
|
||||
|
||||
static_assert(MQTT_DEFAULT_TOPIC_MAX_LEN == 248, "update MQTT_DEFAULT_TOPIC_MAX_LEN in mqtt/__init__.py");
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// mqtt/__init__.py picks this source only for topics that fit
|
||||
static StringRef progmem_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) {
|
||||
size_t len = buf_append_str_p(buf.data(), buf.size(), 0, static_cast<const char *>(context));
|
||||
return StringRef(buf.data(), len);
|
||||
}
|
||||
|
||||
MQTTTopicSource progmem_topic(ProgmemStr topic) {
|
||||
return {progmem_topic_builder, reinterpret_cast<const char *>(topic)};
|
||||
}
|
||||
#endif
|
||||
|
||||
MQTTTopicSource MQTTClientComponent::copy_topic(StringRef topic) {
|
||||
char *copy = new char[topic.size() + 1]; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
memcpy(copy, topic.c_str(), topic.size());
|
||||
copy[topic.size()] = '\0';
|
||||
return {copied_topic_builder, copy};
|
||||
}
|
||||
|
||||
void MQTTClientComponent::add_subscription_(MQTTTopicSource topic, mqtt_subscription_callback_t callback, bool boxed,
|
||||
uint8_t qos) {
|
||||
auto &subscription = this->subscriptions_.emplace_back(MQTTSubscription{
|
||||
.topic = std::move(topic),
|
||||
.topic = topic,
|
||||
.callback = callback,
|
||||
.resubscribe_timeout = 0,
|
||||
.qos = qos,
|
||||
.subscribed = false,
|
||||
.boxed = boxed,
|
||||
.removed = false,
|
||||
.callback = callback,
|
||||
.resubscribe_timeout = 0,
|
||||
});
|
||||
this->resubscribe_subscription_(&subscription);
|
||||
}
|
||||
@@ -535,8 +576,9 @@ void MQTTClientComponent::unsubscribe(const std::string &topic) {
|
||||
this->status_momentary_warning(1000);
|
||||
}
|
||||
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
for (auto &subscription : this->subscriptions_) {
|
||||
if (subscription.topic == topic)
|
||||
if (subscription.topic.build(buf) == topic)
|
||||
subscription.removed = true;
|
||||
}
|
||||
// A callback may be running; the outermost dispatch erases them once it returns.
|
||||
@@ -553,6 +595,8 @@ void MQTTClientComponent::erase_removed_subscriptions_() {
|
||||
if (it->removed) {
|
||||
if (it->boxed)
|
||||
it->callback.free_boxed();
|
||||
if (it->topic.builder == copied_topic_builder)
|
||||
delete[] static_cast<const char *>(it->topic.context); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
it = this->subscriptions_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -629,6 +673,12 @@ void MQTTClientComponent::disable() {
|
||||
this->on_shutdown();
|
||||
}
|
||||
|
||||
// Out of line so the topic buffer is off the stack while the matched callback runs
|
||||
__attribute__((noinline)) static bool topic_matches(const char *message, const MQTTTopicSource &topic) {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return topic_match(message, topic.build(buf).c_str());
|
||||
}
|
||||
|
||||
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
|
||||
#ifdef USE_ESP8266
|
||||
// IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266.
|
||||
@@ -662,7 +712,7 @@ void MQTTClientComponent::on_message(const std::string &topic, const std::string
|
||||
const size_t count = this->subscriptions_.size();
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &subscription = this->subscriptions_[i];
|
||||
if (!subscription.removed && topic_match(topic.c_str(), subscription.topic.c_str()))
|
||||
if (!subscription.removed && topic_matches(topic.c_str(), subscription.topic))
|
||||
subscription.callback.call(topic, payload);
|
||||
}
|
||||
this->dispatching_ = was_dispatching;
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
@@ -22,6 +25,7 @@
|
||||
#endif
|
||||
#include "lwip/ip_addr.h"
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
// The crash log replay needs the logger, which mqtt does not depend on
|
||||
@@ -31,6 +35,18 @@
|
||||
|
||||
namespace esphome::mqtt {
|
||||
|
||||
// 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.
|
||||
// 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;
|
||||
|
||||
/** Callback for MQTT events.
|
||||
*/
|
||||
using mqtt_on_connect_callback_t = std::function<MQTTBackend::on_connect_callback_t>;
|
||||
@@ -46,15 +62,33 @@ using mqtt_json_callback_t ESPDEPRECATED(
|
||||
"subscribe_json() takes any callable; pass the lambda directly. Removed in 2027.5.0",
|
||||
"2026.11.0") = std::function<void(const std::string &, JsonObject)>;
|
||||
|
||||
/// Returns a subscription's topic, written into buf or referring to storage that outlives the subscription.
|
||||
/// Never a null c_str(); an empty topic is "".
|
||||
using mqtt_topic_builder_t = StringRef (*)(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf);
|
||||
|
||||
/// Where a subscription's topic comes from. The topic is rebuilt whenever it is needed, so nothing is stored.
|
||||
struct MQTTTopicSource {
|
||||
mqtt_topic_builder_t builder;
|
||||
const void *context;
|
||||
StringRef build(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const { return this->builder(this->context, buf); }
|
||||
};
|
||||
|
||||
/// A topic string that outlives the subscription, such as a literal; it is referenced, not copied.
|
||||
MQTTTopicSource static_topic(const char *topic);
|
||||
#ifdef USE_ESP8266
|
||||
/// A topic in flash, copied into the buffer when needed, so at most MQTT_DEFAULT_TOPIC_MAX_LEN - 1 characters.
|
||||
MQTTTopicSource progmem_topic(ProgmemStr topic);
|
||||
#endif
|
||||
|
||||
/// internal struct for MQTT subscriptions.
|
||||
struct MQTTSubscription {
|
||||
std::string topic;
|
||||
MQTTTopicSource topic;
|
||||
mqtt_subscription_callback_t callback;
|
||||
uint32_t resubscribe_timeout;
|
||||
uint8_t qos;
|
||||
bool subscribed;
|
||||
bool boxed; // made by create_boxed(), freed on unsubscribe
|
||||
bool removed; // unsubscribed while a callback ran, erased once dispatch returns
|
||||
mqtt_subscription_callback_t callback;
|
||||
uint32_t resubscribe_timeout;
|
||||
};
|
||||
|
||||
/// internal struct for MQTT credentials.
|
||||
@@ -183,32 +217,44 @@ class MQTTClientComponent final : public Component {
|
||||
* A callback that fits in a pointer (capture at most `this`) is stored inline; a larger one is
|
||||
* boxed on the heap and freed by unsubscribe().
|
||||
*
|
||||
* @param topic Where the topic comes from; it is rebuilt when needed instead of stored.
|
||||
* @param callback The callback function.
|
||||
* @param qos The QoS of this subscription.
|
||||
*/
|
||||
template<typename F> void subscribe(MQTTTopicSource topic, F &&callback, uint8_t qos = 0) {
|
||||
constexpr bool boxed = !mqtt_subscription_callback_t::fits_inline<F>();
|
||||
this->add_subscription_(topic,
|
||||
boxed ? mqtt_subscription_callback_t::create_boxed(std::forward<F>(callback))
|
||||
: mqtt_subscription_callback_t::create(std::forward<F>(callback)),
|
||||
boxed, qos);
|
||||
}
|
||||
|
||||
/** Subscribe to an MQTT topic and call callback when a message is received.
|
||||
*
|
||||
* The topic is copied to the heap and freed by unsubscribe().
|
||||
*
|
||||
* @param topic The topic. Wildcards are currently not supported.
|
||||
* @param callback The callback function.
|
||||
* @param qos The QoS of this subscription.
|
||||
*/
|
||||
template<typename F> void subscribe(std::string topic, F &&callback, uint8_t qos = 0) {
|
||||
constexpr bool boxed = !mqtt_subscription_callback_t::fits_inline<F>();
|
||||
this->add_subscription_(std::move(topic),
|
||||
boxed ? mqtt_subscription_callback_t::create_boxed(std::forward<F>(callback))
|
||||
: mqtt_subscription_callback_t::create(std::forward<F>(callback)),
|
||||
boxed, qos);
|
||||
template<typename F> void subscribe(const std::string &topic, F &&callback, uint8_t qos = 0) {
|
||||
this->subscribe(copy_topic(StringRef(topic)), std::forward<F>(callback), qos);
|
||||
}
|
||||
|
||||
/** Subscribe to a MQTT topic and automatically parse JSON payload.
|
||||
*
|
||||
* If an invalid JSON payload is received, the callback will not be called.
|
||||
*
|
||||
* @param topic The topic. Wildcards are currently not supported.
|
||||
* @param topic An MQTTTopicSource, or a string that is copied to the heap.
|
||||
* @param callback The callback with a parsed JsonObject that will be called when a message with matching topic is
|
||||
* received.
|
||||
* @param qos The QoS of this subscription.
|
||||
*/
|
||||
template<typename F> void subscribe_json(std::string topic, F &&callback, uint8_t qos = 0) {
|
||||
template<typename T, typename F> void subscribe_json(T &&topic, F &&callback, uint8_t qos = 0) {
|
||||
static_assert(std::is_invocable_v<const std::decay_t<F> &, const std::string &, JsonObject>,
|
||||
"subscribe_json() needs a callback that does not mutate its captures");
|
||||
this->subscribe(
|
||||
std::move(topic),
|
||||
std::forward<T>(topic),
|
||||
[cb = std::forward<F>(callback)](const std::string &topic, const std::string &payload) {
|
||||
call_json(cb, topic, payload);
|
||||
},
|
||||
@@ -301,6 +347,12 @@ class MQTTClientComponent final : public Component {
|
||||
void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; }
|
||||
|
||||
protected:
|
||||
// For the lambda command topic, which is copied once
|
||||
friend class MQTTComponent;
|
||||
|
||||
/// A heap copy of topic, owned by the one subscription it is passed to and freed when that is erased.
|
||||
static MQTTTopicSource copy_topic(StringRef topic);
|
||||
|
||||
void send_device_info_();
|
||||
|
||||
/// Reconnect to the MQTT broker if not already connected.
|
||||
@@ -317,6 +369,7 @@ class MQTTClientComponent final : public Component {
|
||||
void recalculate_availability_();
|
||||
|
||||
bool subscribe_(const char *topic, uint8_t qos);
|
||||
bool subscribe_topic_(const MQTTTopicSource &topic, uint8_t qos);
|
||||
void resubscribe_subscription_(MQTTSubscription *sub);
|
||||
template<typename F> static void call_json(const F &callback, const std::string &topic, const std::string &payload) {
|
||||
JsonDocument doc = json::parse_json(payload);
|
||||
@@ -328,7 +381,7 @@ class MQTTClientComponent final : public Component {
|
||||
#ifdef USE_MQTT_CRASH_LOG
|
||||
void handle_crash_log_request_(const std::string &topic, const std::string &payload);
|
||||
#endif
|
||||
void add_subscription_(std::string &&topic, mqtt_subscription_callback_t callback, bool boxed, uint8_t qos);
|
||||
void add_subscription_(MQTTTopicSource topic, mqtt_subscription_callback_t callback, bool boxed, uint8_t qos);
|
||||
void resubscribe_subscriptions_();
|
||||
|
||||
MQTTCredentials credentials_;
|
||||
|
||||
@@ -209,7 +209,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
}
|
||||
void MQTTClimateComponent::setup() {
|
||||
auto traits = this->device_->get_traits();
|
||||
this->subscribe(this->get_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->get_mode_command_topic_source(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->device_->make_call();
|
||||
call.set_mode(payload);
|
||||
call.perform();
|
||||
@@ -217,7 +217,7 @@ void MQTTClimateComponent::setup() {
|
||||
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
this->subscribe(this->get_target_temperature_low_command_topic(),
|
||||
this->subscribe(this->get_target_temperature_low_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_number<float>(payload);
|
||||
if (!val.has_value()) {
|
||||
@@ -228,7 +228,7 @@ void MQTTClimateComponent::setup() {
|
||||
call.set_target_temperature_low(*val);
|
||||
call.perform();
|
||||
});
|
||||
this->subscribe(this->get_target_temperature_high_command_topic(),
|
||||
this->subscribe(this->get_target_temperature_high_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_number<float>(payload);
|
||||
if (!val.has_value()) {
|
||||
@@ -240,7 +240,7 @@ void MQTTClimateComponent::setup() {
|
||||
call.perform();
|
||||
});
|
||||
} else {
|
||||
this->subscribe(this->get_target_temperature_command_topic(),
|
||||
this->subscribe(this->get_target_temperature_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_number<float>(payload);
|
||||
if (!val.has_value()) {
|
||||
@@ -254,7 +254,7 @@ void MQTTClimateComponent::setup() {
|
||||
}
|
||||
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
this->subscribe(this->get_target_humidity_command_topic(),
|
||||
this->subscribe(this->get_target_humidity_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_number<float>(payload);
|
||||
if (!val.has_value()) {
|
||||
@@ -268,27 +268,30 @@ void MQTTClimateComponent::setup() {
|
||||
}
|
||||
|
||||
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
|
||||
this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->device_->make_call();
|
||||
call.set_preset(payload);
|
||||
call.perform();
|
||||
});
|
||||
this->subscribe(this->get_preset_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->device_->make_call();
|
||||
call.set_preset(payload);
|
||||
call.perform();
|
||||
});
|
||||
}
|
||||
|
||||
if (traits.get_supports_fan_modes()) {
|
||||
this->subscribe(this->get_fan_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->device_->make_call();
|
||||
call.set_fan_mode(payload);
|
||||
call.perform();
|
||||
});
|
||||
this->subscribe(this->get_fan_mode_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->device_->make_call();
|
||||
call.set_fan_mode(payload);
|
||||
call.perform();
|
||||
});
|
||||
}
|
||||
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
this->subscribe(this->get_swing_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->device_->make_call();
|
||||
call.set_swing_mode(payload);
|
||||
call.perform();
|
||||
});
|
||||
this->subscribe(this->get_swing_mode_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->device_->make_call();
|
||||
call.set_swing_mode(payload);
|
||||
call.perform();
|
||||
});
|
||||
}
|
||||
|
||||
this->device_->add_on_state_callback([this](Climate & /*unused*/) { this->publish_state_(); });
|
||||
|
||||
@@ -124,6 +124,14 @@ StringRef MQTTComponent::get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPI
|
||||
return this->get_default_topic_for_to_(buf, "command", 7);
|
||||
}
|
||||
|
||||
MQTTTopicSource MQTTComponent::command_topic_source_() const {
|
||||
if (this->custom_command_topic_.is_lambda()) {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return MQTTClientComponent::copy_topic(this->get_command_topic_to_(buf));
|
||||
}
|
||||
return this->topic_source_<MQTTComponent, &MQTTComponent::get_command_topic_to_>();
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_state_topic_() const {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_state_topic_to_(buf);
|
||||
|
||||
@@ -21,23 +21,14 @@ 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.
|
||||
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;
|
||||
static constexpr size_t MQTT_DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
class MQTTComponent; // Forward declaration
|
||||
|
||||
// Validated in Python: cv.Length(max=64), DISCOVERY_PREFIX_MAX_LEN in mqtt/__init__.py
|
||||
static constexpr size_t MQTT_DISCOVERY_PREFIX_MAX_LEN = 64;
|
||||
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
|
||||
static constexpr size_t MQTT_DISCOVERY_TOPIC_MAX_LEN = MQTT_DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN +
|
||||
1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 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)
|
||||
@@ -49,7 +40,9 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
|
||||
static_assert(sizeof(type_str) - 1 <= MQTT_COMPONENT_TYPE_MAX_LEN, \
|
||||
#class_name "::component_type() exceeds MQTT_COMPONENT_TYPE_MAX_LEN");
|
||||
|
||||
// Macro to define custom topic getter/setter with compile-time suffix length verification
|
||||
// Macro to define custom topic getter/setter with compile-time suffix length verification.
|
||||
// Set the topic before setup(): subscriptions rebuild it when needed, so a later change only reaches the broker
|
||||
// after a reconnect.
|
||||
#define MQTT_COMPONENT_CUSTOM_TOPIC_(name, type) \
|
||||
static_assert(sizeof(#name "/" #type) - 1 <= MQTT_SUFFIX_MAX_LEN, \
|
||||
"topic suffix " #name "/" #type " exceeds MQTT_SUFFIX_MAX_LEN"); \
|
||||
@@ -64,6 +57,10 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
|
||||
return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \
|
||||
return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \
|
||||
} \
|
||||
MQTTTopicSource get_##name##_##type##_topic_source() const { \
|
||||
using Self = std::remove_cvref_t<decltype(*this)>; \
|
||||
return this->topic_source_<Self, &Self::get_##name##_##type##_topic_to>(); \
|
||||
} \
|
||||
std::string get_##name##_##type##_topic() const { \
|
||||
if (this->custom_##name##_##type##_topic_.empty()) \
|
||||
return this->get_default_topic_for_(#name "/" #type); \
|
||||
@@ -128,6 +125,7 @@ class MQTTComponent : public Component {
|
||||
template<typename T> void set_custom_state_topic(T &&custom_state_topic) {
|
||||
this->custom_state_topic_ = std::forward<T>(custom_state_topic);
|
||||
}
|
||||
/// Set a custom command topic before setup(); a later change only reaches the broker after a reconnect.
|
||||
template<typename T> void set_custom_command_topic(T &&custom_command_topic) {
|
||||
this->custom_command_topic_ = std::forward<T>(custom_command_topic);
|
||||
}
|
||||
@@ -255,25 +253,25 @@ class MQTTComponent : public Component {
|
||||
|
||||
/** Subscribe to a MQTT topic.
|
||||
*
|
||||
* @param topic The topic. Wildcards are currently not supported.
|
||||
* @param topic The topic: an MQTTTopicSource, or a string that is copied to the heap.
|
||||
* @param callback The callback that will be called when a message with matching topic is received.
|
||||
* @param qos The MQTT quality of service. Defaults to 0.
|
||||
*/
|
||||
template<typename F> void subscribe(std::string topic, F &&callback, uint8_t qos = 0) {
|
||||
global_mqtt_client->subscribe(std::move(topic), std::forward<F>(callback), qos);
|
||||
template<typename T, typename F> void subscribe(T &&topic, F &&callback, uint8_t qos = 0) {
|
||||
global_mqtt_client->subscribe(std::forward<T>(topic), std::forward<F>(callback), qos);
|
||||
}
|
||||
|
||||
/** Subscribe to a MQTT topic and automatically parse JSON payload.
|
||||
*
|
||||
* If an invalid JSON payload is received, the callback will not be called.
|
||||
*
|
||||
* @param topic The topic. Wildcards are currently not supported.
|
||||
* @param topic The topic: an MQTTTopicSource, or a string that is copied to the heap.
|
||||
* @param callback The callback with a parsed JsonObject that will be called when a message with matching topic is
|
||||
* received.
|
||||
* @param qos The MQTT quality of service. Defaults to 0.
|
||||
*/
|
||||
template<typename F> void subscribe_json(std::string topic, F &&callback, uint8_t qos = 0) {
|
||||
global_mqtt_client->subscribe_json(std::move(topic), std::forward<F>(callback), qos);
|
||||
template<typename T, typename F> void subscribe_json(T &&topic, F &&callback, uint8_t qos = 0) {
|
||||
global_mqtt_client->subscribe_json(std::forward<T>(topic), std::forward<F>(callback), qos);
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -325,6 +323,18 @@ class MQTTComponent : public Component {
|
||||
/// @return StringRef pointing to the topic in the buffer.
|
||||
StringRef get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
|
||||
|
||||
/// The command topic as a subscription source. A lambda topic is read once and copied, as it may change.
|
||||
MQTTTopicSource command_topic_source_() const;
|
||||
|
||||
/// A topic built by one of this component's `get_*_topic_to` getters, rebuilt when needed.
|
||||
template<typename C, StringRef (C::*Getter)(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN>) const>
|
||||
MQTTTopicSource topic_source_() const {
|
||||
return {[](const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) {
|
||||
return (static_cast<const C *>(context)->*Getter)(buf);
|
||||
},
|
||||
static_cast<const C *>(this)};
|
||||
}
|
||||
|
||||
/// Get the MQTT topic that new states will be shared to (allocates std::string).
|
||||
std::string get_state_topic_() const;
|
||||
|
||||
|
||||
@@ -31,34 +31,36 @@ MQTTCoverComponent::MQTTCoverComponent(Cover *cover) : cover_(cover) {}
|
||||
void MQTTCoverComponent::setup() {
|
||||
auto traits = this->cover_->get_traits();
|
||||
this->cover_->add_on_state_callback([this]() { this->publish_state(); });
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->cover_->make_call();
|
||||
call.set_command(payload.c_str());
|
||||
call.perform();
|
||||
});
|
||||
if (traits.get_supports_position()) {
|
||||
this->subscribe(this->get_position_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto value = parse_number<float>(payload);
|
||||
if (!value.has_value()) {
|
||||
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
|
||||
return;
|
||||
}
|
||||
auto call = this->cover_->make_call();
|
||||
call.set_position(*value / 100.0f);
|
||||
call.perform();
|
||||
});
|
||||
this->subscribe(this->get_position_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto value = parse_number<float>(payload);
|
||||
if (!value.has_value()) {
|
||||
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
|
||||
return;
|
||||
}
|
||||
auto call = this->cover_->make_call();
|
||||
call.set_position(*value / 100.0f);
|
||||
call.perform();
|
||||
});
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
this->subscribe(this->get_tilt_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto value = parse_number<float>(payload);
|
||||
if (!value.has_value()) {
|
||||
ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str());
|
||||
return;
|
||||
}
|
||||
auto call = this->cover_->make_call();
|
||||
call.set_tilt(*value / 100.0f);
|
||||
call.perform();
|
||||
});
|
||||
this->subscribe(this->get_tilt_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto value = parse_number<float>(payload);
|
||||
if (!value.has_value()) {
|
||||
ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str());
|
||||
return;
|
||||
}
|
||||
auto call = this->cover_->make_call();
|
||||
call.set_tilt(*value / 100.0f);
|
||||
call.perform();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ using namespace esphome::datetime;
|
||||
MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
|
||||
|
||||
void MQTTDateComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
this->subscribe_json(this->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->date_->make_call();
|
||||
if (root[ESPHOME_F("year")].is<uint16_t>()) {
|
||||
call.set_year(root[ESPHOME_F("year")]);
|
||||
|
||||
@@ -17,7 +17,7 @@ using namespace esphome::datetime;
|
||||
MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetime_(datetime) {}
|
||||
|
||||
void MQTTDateTimeComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
this->subscribe_json(this->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->datetime_->make_call();
|
||||
if (root[ESPHOME_F("year")].is<uint16_t>()) {
|
||||
call.set_year(root[ESPHOME_F("year")]);
|
||||
|
||||
@@ -28,7 +28,7 @@ MQTT_COMPONENT_TYPE(MQTTFanComponent, "fan")
|
||||
const EntityBase *MQTTFanComponent::get_entity() const { return this->state_; }
|
||||
|
||||
void MQTTFanComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_on_off(payload.c_str());
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
@@ -52,33 +52,34 @@ void MQTTFanComponent::setup() {
|
||||
});
|
||||
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
this->subscribe(this->get_direction_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_on_off(payload.c_str(), "forward", "reverse");
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", LOG_STR_ARG(this->log_name_()));
|
||||
this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform();
|
||||
break;
|
||||
case PARSE_OFF:
|
||||
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", LOG_STR_ARG(this->log_name_()));
|
||||
this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform();
|
||||
break;
|
||||
case PARSE_TOGGLE:
|
||||
this->state_->make_call()
|
||||
.set_direction(this->state_->direction == fan::FanDirection::FORWARD ? fan::FanDirection::REVERSE
|
||||
: fan::FanDirection::FORWARD)
|
||||
.perform();
|
||||
break;
|
||||
case PARSE_NONE:
|
||||
ESP_LOGW(TAG, "Unknown direction Payload %s", payload.c_str());
|
||||
this->status_momentary_warning(5000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this->subscribe(
|
||||
this->get_direction_command_topic_source(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_on_off(payload.c_str(), "forward", "reverse");
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", LOG_STR_ARG(this->log_name_()));
|
||||
this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform();
|
||||
break;
|
||||
case PARSE_OFF:
|
||||
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", LOG_STR_ARG(this->log_name_()));
|
||||
this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform();
|
||||
break;
|
||||
case PARSE_TOGGLE:
|
||||
this->state_->make_call()
|
||||
.set_direction(this->state_->direction == fan::FanDirection::FORWARD ? fan::FanDirection::REVERSE
|
||||
: fan::FanDirection::FORWARD)
|
||||
.perform();
|
||||
break;
|
||||
case PARSE_NONE:
|
||||
ESP_LOGW(TAG, "Unknown direction Payload %s", payload.c_str());
|
||||
this->status_momentary_warning(5000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this->state_->get_traits().supports_oscillation()) {
|
||||
this->subscribe(this->get_oscillation_command_topic(),
|
||||
this->subscribe(this->get_oscillation_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off");
|
||||
switch (val) {
|
||||
@@ -102,7 +103,7 @@ void MQTTFanComponent::setup() {
|
||||
}
|
||||
|
||||
if (this->state_->get_traits().supports_speed()) {
|
||||
this->subscribe(this->get_speed_level_command_topic(),
|
||||
this->subscribe(this->get_speed_level_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
optional<int> speed_level_opt = parse_number<int>(payload);
|
||||
if (speed_level_opt.has_value()) {
|
||||
|
||||
@@ -18,7 +18,7 @@ MQTT_COMPONENT_TYPE(MQTTJSONLightComponent, "light")
|
||||
const EntityBase *MQTTJSONLightComponent::get_entity() const { return this->state_; }
|
||||
|
||||
void MQTTJSONLightComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
this->subscribe_json(this->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
|
||||
LightCall call = this->state_->make_call();
|
||||
LightJSONSchema::parse_json(*this->state_, call, root);
|
||||
call.perform();
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace esphome::lock;
|
||||
MQTTLockComponent::MQTTLockComponent(lock::Lock *a_lock) : lock_(a_lock) {}
|
||||
|
||||
void MQTTLockComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
|
||||
if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("LOCK")) == 0) {
|
||||
this->lock_->lock();
|
||||
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("UNLOCK")) == 0) {
|
||||
|
||||
@@ -19,7 +19,7 @@ PROGMEM_STRING_TABLE(NumberMqttModeStrings, "", "box", "slider");
|
||||
MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {}
|
||||
|
||||
void MQTTNumberComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
|
||||
auto val = parse_number<float>(state);
|
||||
if (!val.has_value()) {
|
||||
ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str());
|
||||
|
||||
@@ -15,7 +15,7 @@ using namespace esphome::select;
|
||||
MQTTSelectComponent::MQTTSelectComponent(Select *select) : select_(select) {}
|
||||
|
||||
void MQTTSelectComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
|
||||
auto call = this->select_->make_call();
|
||||
call.set_option(state);
|
||||
call.perform();
|
||||
|
||||
@@ -15,7 +15,7 @@ using namespace esphome::switch_;
|
||||
MQTTSwitchComponent::MQTTSwitchComponent(switch_::Switch *a_switch) : switch_(a_switch) {}
|
||||
|
||||
void MQTTSwitchComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
|
||||
switch (parse_on_off(payload.c_str())) {
|
||||
case PARSE_ON:
|
||||
this->switch_->turn_on();
|
||||
|
||||
@@ -19,7 +19,7 @@ PROGMEM_STRING_TABLE(TextMqttModeStrings, "text", "password");
|
||||
MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {}
|
||||
|
||||
void MQTTTextComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
|
||||
auto call = this->text_->make_call();
|
||||
call.set_value(state);
|
||||
call.perform();
|
||||
|
||||
@@ -17,7 +17,7 @@ using namespace esphome::datetime;
|
||||
MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
|
||||
|
||||
void MQTTTimeComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
this->subscribe_json(this->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->time_->make_call();
|
||||
if (root[ESPHOME_F("hour")].is<uint8_t>()) {
|
||||
call.set_hour(root[ESPHOME_F("hour")]);
|
||||
|
||||
@@ -15,7 +15,7 @@ using namespace esphome::update;
|
||||
MQTTUpdateComponent::MQTTUpdateComponent(UpdateEntity *update) : update_(update) {}
|
||||
|
||||
void MQTTUpdateComponent::setup() {
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
|
||||
if (payload == "INSTALL") {
|
||||
this->update_->perform();
|
||||
} else {
|
||||
|
||||
@@ -31,22 +31,23 @@ MQTTValveComponent::MQTTValveComponent(Valve *valve) : valve_(valve) {}
|
||||
void MQTTValveComponent::setup() {
|
||||
auto traits = this->valve_->get_traits();
|
||||
this->valve_->add_on_state_callback([this]() { this->publish_state(); });
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->valve_->make_call();
|
||||
call.set_command(payload.c_str());
|
||||
call.perform();
|
||||
});
|
||||
if (traits.get_supports_position()) {
|
||||
this->subscribe(this->get_position_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto value = parse_number<float>(payload);
|
||||
if (!value.has_value()) {
|
||||
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
|
||||
return;
|
||||
}
|
||||
auto call = this->valve_->make_call();
|
||||
call.set_position(*value / 100.0f);
|
||||
call.perform();
|
||||
});
|
||||
this->subscribe(this->get_position_command_topic_source(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto value = parse_number<float>(payload);
|
||||
if (!value.has_value()) {
|
||||
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
|
||||
return;
|
||||
}
|
||||
auto call = this->valve_->make_call();
|
||||
call.set_position(*value / 100.0f);
|
||||
call.perform();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -392,6 +392,8 @@ template<typename... X> class TemplatableValue<std::string, X...> {
|
||||
}
|
||||
|
||||
bool has_value() const { return this->type_ != NONE; }
|
||||
/// Whether the value comes from a lambda, so it may differ between reads.
|
||||
bool is_lambda() const { return this->type_ == LAMBDA || this->type_ == STATELESS_LAMBDA; }
|
||||
|
||||
std::string value(X... x) const {
|
||||
switch (this->type_) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Tests for choosing where an mqtt trigger topic is stored."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.mqtt import MQTT_DEFAULT_TOPIC_MAX_LEN, _topic_source
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
# "é" is two bytes in UTF-8, so these topics are one character shorter than their byte length
|
||||
LIMIT = MQTT_DEFAULT_TOPIC_MAX_LEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("topic", "platform", "source"),
|
||||
[
|
||||
pytest.param("t" * (LIMIT - 1), PLATFORM_ESP8266, "progmem_topic", id="fits"),
|
||||
pytest.param("t" * LIMIT, PLATFORM_ESP8266, "static_topic", id="too_long"),
|
||||
pytest.param(
|
||||
"é" + "t" * (LIMIT - 3), PLATFORM_ESP8266, "progmem_topic", id="utf8_fits"
|
||||
),
|
||||
pytest.param(
|
||||
"é" + "t" * (LIMIT - 2),
|
||||
PLATFORM_ESP8266,
|
||||
"static_topic",
|
||||
id="utf8_bytes_too_long",
|
||||
),
|
||||
pytest.param("t" * (LIMIT - 1), PLATFORM_ESP32, "static_topic", id="esp32"),
|
||||
pytest.param("é" * 10, PLATFORM_ESP32, "static_topic", id="esp32_utf8"),
|
||||
],
|
||||
)
|
||||
def test_topic_source(
|
||||
monkeypatch: pytest.MonkeyPatch, topic: str, platform: str, source: str
|
||||
) -> None:
|
||||
"""ESP8266 keeps a topic in flash only when its UTF-8 bytes fit the copy buffer."""
|
||||
monkeypatch.setitem(CORE.data, KEY_CORE, {KEY_TARGET_PLATFORM: platform})
|
||||
assert str(_topic_source(topic)).startswith(f"mqtt::{source}(")
|
||||
Reference in New Issue
Block a user