1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-09 09:11:52 +00:00

Compare commits

..

5 Commits

15 changed files with 215 additions and 136 deletions

View File

@@ -47,7 +47,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
root[ESPHOME_F("schema")] = ESPHOME_F("json");
auto traits = this->state_->get_traits();
root[MQTT_COLOR_MODE] = true;
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray color_modes = root[ESPHOME_F("supported_color_modes")].to<JsonArray>();
if (traits.supports_color_mode(ColorMode::ON_OFF))
@@ -68,10 +67,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE))
color_modes.add(ESPHOME_F("rgbww"));
// legacy API
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
root[ESPHOME_F("brightness")] = true;
if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) ||
traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) {
root[MQTT_MIN_MIREDS] = traits.get_min_mireds();

View File

@@ -10,35 +10,65 @@ from esphome.const import (
CONF_OPTIONS,
CONF_RESTORE_VALUE,
CONF_SET_ACTION,
CONF_UPDATE_INTERVAL,
SCHEDULER_DONT_RUN,
)
from esphome.core import TimePeriodMilliseconds
from esphome.cpp_generator import TemplateArguments
from .. import template_ns
TemplateSelect = template_ns.class_(
"TemplateSelect", select.Select, cg.PollingComponent
)
TemplateSelectWithSetAction = template_ns.class_(
"TemplateSelectWithSetAction", TemplateSelect
)
def validate(config):
errors = []
if CONF_LAMBDA in config:
if config[CONF_OPTIMISTIC]:
raise cv.Invalid("optimistic cannot be used with lambda")
errors.append(
cv.Invalid(
"optimistic cannot be used with lambda", path=[CONF_OPTIMISTIC]
)
)
if CONF_INITIAL_OPTION in config:
raise cv.Invalid("initial_value cannot be used with lambda")
errors.append(
cv.Invalid(
"initial_value cannot be used with lambda",
path=[CONF_INITIAL_OPTION],
)
)
if CONF_RESTORE_VALUE in config:
raise cv.Invalid("restore_value cannot be used with lambda")
errors.append(
cv.Invalid(
"restore_value cannot be used with lambda",
path=[CONF_RESTORE_VALUE],
)
)
elif CONF_INITIAL_OPTION in config:
if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]:
raise cv.Invalid(
f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]"
errors.append(
cv.Invalid(
f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]",
path=[CONF_INITIAL_OPTION],
)
)
else:
config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0]
if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config:
raise cv.Invalid(
"Either optimistic mode must be enabled, or set_action must be set, to handle the option being set."
errors.append(
cv.Invalid(
"Either optimistic mode must be enabled, or set_action must be set, to handle the option being set."
)
)
if errors:
raise cv.MultipleInvalid(errors)
return config
@@ -62,29 +92,34 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await select.register_select(var, config, options=config[CONF_OPTIONS])
var_id = config[CONF_ID]
if CONF_SET_ACTION in config:
var_id.type = TemplateSelectWithSetAction
has_lambda = CONF_LAMBDA in config
optimistic = config.get(CONF_OPTIMISTIC, False)
restore_value = config.get(CONF_RESTORE_VALUE, False)
options = config[CONF_OPTIONS]
initial_option = config.get(CONF_INITIAL_OPTION, 0)
initial_option_index = options.index(initial_option) if not has_lambda else 0
var = cg.new_Pvariable(
var_id,
TemplateArguments(has_lambda, optimistic, restore_value, initial_option_index),
)
component_config = config.copy()
if not has_lambda:
# No point in polling if not using a lambda
component_config[CONF_UPDATE_INTERVAL] = TimePeriodMilliseconds(
milliseconds=SCHEDULER_DONT_RUN
)
await cg.register_component(var, component_config)
await select.register_select(var, config, options=options)
if CONF_LAMBDA in config:
template_ = await cg.process_lambda(
lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string)
)
cg.add(var.set_template(template_))
else:
# Only set if non-default to avoid bloating setup() function
if config[CONF_OPTIMISTIC]:
cg.add(var.set_optimistic(True))
initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION])
# Only set if non-zero to avoid bloating setup() function
# (initial_option_index_ is zero-initialized in the header)
if initial_option_index != 0:
cg.add(var.set_initial_option_index(initial_option_index))
# Only set if True (default is False)
if config.get(CONF_RESTORE_VALUE):
cg.add(var.set_restore_value(True))
cg.add(var.set_lambda(lambda_))
if CONF_SET_ACTION in config:
await automation.build_automation(

View File

@@ -5,61 +5,44 @@ namespace esphome::template_ {
static const char *const TAG = "template.select";
void TemplateSelect::setup() {
if (this->f_.has_value())
return;
size_t index = this->initial_option_index_;
if (this->restore_value_) {
this->pref_ = this->make_entity_preference<size_t>();
size_t restored_index;
if (this->pref_.load(&restored_index) && this->has_index(restored_index)) {
index = restored_index;
ESP_LOGD(TAG, "State from restore: %s", this->option_at(index));
} else {
ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index));
}
void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda,
const size_t initial_option_index, bool restore_value) {
LOG_SELECT("", "Template Select", sel_comp);
if (has_lambda) {
LOG_UPDATE_INTERVAL(sel_comp);
} else {
ESP_LOGD(TAG, "State from initial: %s", this->option_at(index));
ESP_LOGCONFIG(TAG,
" Optimistic: %s\n"
" Initial Option: %s\n"
" Restore Value: %s",
YESNO(optimistic), sel_comp->option_at(initial_option_index), YESNO(restore_value));
}
this->publish_state(index);
}
void TemplateSelect::update() {
if (!this->f_.has_value())
return;
void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index) {
ESP_LOGD(TAG, "State from initial: %s", sel_comp->option_at(initial_index));
sel_comp->publish_state(initial_index);
}
auto val = this->f_();
void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index) {
size_t index = initial_index;
if (pref.load(&index) && sel_comp->has_index(index)) {
ESP_LOGD(TAG, "State from restore: %s", sel_comp->option_at(index));
} else {
index = initial_index;
ESP_LOGD(TAG, "State from initial (no valid stored index): %s", sel_comp->option_at(initial_index));
}
sel_comp->publish_state(index);
}
void update_lambda(BaseTemplateSelect *sel_comp, const optional<std::string> &val) {
if (val.has_value()) {
if (!this->has_option(*val)) {
if (!sel_comp->has_option(*val)) {
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
return;
}
this->publish_state(*val);
sel_comp->publish_state(*val);
}
}
void TemplateSelect::control(size_t index) {
this->set_trigger_->trigger(StringRef(this->option_at(index)));
if (this->optimistic_)
this->publish_state(index);
if (this->restore_value_)
this->pref_.save(&index);
}
void TemplateSelect::dump_config() {
LOG_SELECT("", "Template Select", this);
LOG_UPDATE_INTERVAL(this);
if (this->f_.has_value())
return;
ESP_LOGCONFIG(TAG,
" Optimistic: %s\n"
" Initial Option: %s\n"
" Restore Value: %s",
YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_));
}
} // namespace esphome::template_

View File

@@ -9,29 +9,71 @@
namespace esphome::template_ {
class TemplateSelect final : public select::Select, public PollingComponent {
public:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
struct Empty {};
class BaseTemplateSelect : public select::Select, public PollingComponent {};
void setup() override;
void update() override;
void dump_config() override;
void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index,
bool restore_value);
void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index);
void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index);
void update_lambda(BaseTemplateSelect *sel_comp, const optional<std::string> &val);
/// Base template select class - used when no set_action is configured
template<bool HAS_LAMBDA, bool OPTIMISTIC, bool RESTORE_VALUE, size_t INITIAL_OPTION_INDEX>
class TemplateSelect : public BaseTemplateSelect {
public:
template<typename F> void set_lambda(F &&f) {
if constexpr (HAS_LAMBDA) {
this->f_.set(std::forward<F>(f));
}
}
void setup() override {
if constexpr (!HAS_LAMBDA) {
if constexpr (RESTORE_VALUE) {
this->pref_ = this->template make_entity_preference<size_t>();
setup_with_restore(this, this->pref_, INITIAL_OPTION_INDEX);
} else {
setup_initial(this, INITIAL_OPTION_INDEX);
}
}
}
void update() override {
if constexpr (HAS_LAMBDA) {
update_lambda(this, this->f_());
}
}
void dump_config() override {
dump_config_helper(this, OPTIMISTIC, HAS_LAMBDA, INITIAL_OPTION_INDEX, RESTORE_VALUE);
};
float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<StringRef> *get_set_trigger() const { return this->set_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; }
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
protected:
void control(size_t index) override {
if constexpr (OPTIMISTIC)
this->publish_state(index);
if constexpr (RESTORE_VALUE)
this->pref_.save(&index);
}
[[no_unique_address]] std::conditional_t<HAS_LAMBDA, TemplateLambda<std::string>, Empty> f_{};
[[no_unique_address]] std::conditional_t<RESTORE_VALUE, ESPPreferenceObject, Empty> pref_{};
};
/// Template select with set_action trigger - only instantiated when set_action is configured
template<bool HAS_LAMBDA, bool OPTIMISTIC, bool RESTORE_VALUE, size_t INITIAL_OPTION_INDEX>
class TemplateSelectWithSetAction final
: public TemplateSelect<HAS_LAMBDA, OPTIMISTIC, RESTORE_VALUE, INITIAL_OPTION_INDEX> {
public:
Trigger<StringRef> *get_set_trigger() { return &this->set_trigger_; }
protected:
void control(size_t index) override;
bool optimistic_ = false;
size_t initial_option_index_{0};
bool restore_value_ = false;
Trigger<StringRef> *set_trigger_ = new Trigger<StringRef>();
TemplateLambda<std::string> f_;
ESPPreferenceObject pref_;
void control(size_t index) override {
this->set_trigger_.trigger(StringRef(this->option_at(index)));
TemplateSelect<HAS_LAMBDA, OPTIMISTIC, RESTORE_VALUE, INITIAL_OPTION_INDEX>::control(index);
}
Trigger<StringRef> set_trigger_;
};
} // namespace esphome::template_

View File

@@ -15,8 +15,6 @@
#include "StreamString.h"
#endif
#include <chrono>
#include <cinttypes>
#include <cstdlib>
#ifdef USE_LIGHT
@@ -367,13 +365,7 @@ void WebServer::set_css_include(const char *css_include) { this->css_include_ =
void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
#endif
/// Get uptime in milliseconds using std::chrono::steady_clock (64-bit, no rollover)
static int64_t get_uptime_ms() {
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch())
.count();
}
json::SerializationBuffer<> WebServer::get_config_json() {
std::string WebServer::get_config_json() {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -388,7 +380,6 @@ json::SerializationBuffer<> WebServer::get_config_json() {
#endif
root[ESPHOME_F("log")] = this->expose_log_;
root[ESPHOME_F("lang")] = "en";
root[ESPHOME_F("uptime")] = get_uptime_ms();
return builder.serialize();
}
@@ -412,11 +403,7 @@ void WebServer::setup() {
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
// getting a lot of events
this->set_interval(10000, [this]() {
char buf[32];
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRId64 "}", get_uptime_ms());
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
});
this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); });
}
void WebServer::loop() { this->events_.loop(); }

View File

@@ -73,18 +73,15 @@ optional<std::string> request_get_url_query(httpd_req_t *req) {
return {str};
}
optional<std::string> query_key_value(const std::string &query_url, const std::string &key) {
if (query_url.empty()) {
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) {
if (query_url == nullptr || query_len == 0) {
return {};
}
auto val = std::unique_ptr<char[]>(new char[query_url.size()]);
if (!val) {
ESP_LOGE(TAG, "Not enough memory to the query key value");
return {};
}
// Use stack buffer for typical query strings, heap fallback for large ones
SmallBufferWithHeapFallback<256, char> val(query_len);
if (httpd_query_key_value(query_url.c_str(), key.c_str(), val.get(), query_url.size()) != ESP_OK) {
if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) {
return {};
}

View File

@@ -15,7 +15,10 @@ size_t url_decode(char *str);
bool request_has_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_url_query(httpd_req_t *req);
optional<std::string> query_key_value(const std::string &query_url, const std::string &key);
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key);
inline optional<std::string> query_key_value(const std::string &query_url, const std::string &key) {
return query_key_value(query_url.c_str(), query_url.size(), key.c_str());
}
// Helper function for case-insensitive character comparison
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }

View File

@@ -366,7 +366,7 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
}
#endif
AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
// Check cache first - only successful lookups are cached
for (auto *param : this->params_) {
if (param->name() == name) {
@@ -375,11 +375,11 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
}
// Look up value from query strings
optional<std::string> val = query_key_value(this->post_query_, name);
optional<std::string> val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name);
if (!val.has_value()) {
auto url_query = request_get_url_query(*this);
if (url_query.has_value()) {
val = query_key_value(url_query.value(), name);
val = query_key_value(url_query.value().c_str(), url_query.value().size(), name);
}
}

View File

@@ -162,19 +162,24 @@ class AsyncWebServerRequest {
}
// NOLINTNEXTLINE(readability-identifier-naming)
bool hasParam(const std::string &name) { return this->getParam(name) != nullptr; }
bool hasParam(const char *name) { return this->getParam(name) != nullptr; }
// NOLINTNEXTLINE(readability-identifier-naming)
AsyncWebParameter *getParam(const std::string &name);
bool hasParam(const std::string &name) { return this->getParam(name.c_str()) != nullptr; }
// NOLINTNEXTLINE(readability-identifier-naming)
AsyncWebParameter *getParam(const char *name);
// NOLINTNEXTLINE(readability-identifier-naming)
AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); }
// NOLINTNEXTLINE(readability-identifier-naming)
bool hasArg(const char *name) { return this->hasParam(name); }
std::string arg(const std::string &name) {
std::string arg(const char *name) {
auto *param = this->getParam(name);
if (param) {
return param->value();
}
return {};
}
std::string arg(const std::string &name) { return this->arg(name.c_str()); }
operator httpd_req_t *() const { return this->req_; }
optional<std::string> get_header(const char *name) const;

View File

@@ -1,4 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml
uart_2400: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml
<<: !include common-generic.yaml

View File

@@ -1,4 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml
uart_2400: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml
<<: !include common-netznoe.yaml

View File

@@ -1,4 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml
uart_2400: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml
<<: !include common-generic.yaml

View File

@@ -296,6 +296,16 @@ select:
// Migration guide: Store in std::string
std::string stored_option(id(template_select).current_option());
ESP_LOGI("test", "Stored: %s", stored_option.c_str());
- platform: template
id: template_select_with_action
name: "Template select with action"
options:
- option_a
- option_b
set_action:
- logger.log:
format: "Selected: %s"
args: ["x.c_str()"]
lock:
- platform: template

View File

@@ -56,7 +56,21 @@ select:
std::string prefix = x.substr(0, 6);
ESP_LOGI("test", "Substr prefix: %s", prefix.c_str());
# Second select with numeric options to test ADL functions
# Second select with set_action trigger (uses TemplateSelectWithSetAction subclass)
- platform: template
name: "Action Select"
id: action_select
options:
- "Action A"
- "Action B"
set_action:
then:
# Test: set_action trigger receives StringRef
- logger.log:
format: "set_action triggered: %s"
args: ['x.c_str()']
# Third select with numeric options to test ADL functions
- platform: template
name: "Baud Rate"
id: baud_select

View File

@@ -28,6 +28,8 @@ async def test_select_stringref_trigger(
find_substr_future = loop.create_future()
find_char_future = loop.create_future()
substr_future = loop.create_future()
# set_action trigger (TemplateSelectWithSetAction subclass)
set_action_future = loop.create_future()
# ADL functions
stoi_future = loop.create_future()
stol_future = loop.create_future()
@@ -43,6 +45,8 @@ async def test_select_stringref_trigger(
find_substr_pattern = re.compile(r"Found 'Option' in value")
find_char_pattern = re.compile(r"Space at position: 6") # space at index 6
substr_pattern = re.compile(r"Substr prefix: Option")
# set_action trigger pattern (TemplateSelectWithSetAction subclass)
set_action_pattern = re.compile(r"set_action triggered: Action B")
# ADL function patterns (115200 from baud rate select)
stoi_pattern = re.compile(r"stoi result: 115200")
stol_pattern = re.compile(r"stol result: 115200")
@@ -67,6 +71,9 @@ async def test_select_stringref_trigger(
find_char_future.set_result(True)
if not substr_future.done() and substr_pattern.search(line):
substr_future.set_result(True)
# set_action trigger
if not set_action_future.done() and set_action_pattern.search(line):
set_action_future.set_result(True)
# ADL functions
if not stoi_future.done() and stoi_pattern.search(line):
stoi_future.set_result(True)
@@ -89,22 +96,21 @@ async def test_select_stringref_trigger(
# List entities to find our select
entities, _ = await client.list_entities_services()
select_entity = next(
(e for e in entities if hasattr(e, "options") and e.name == "Test Select"),
None,
)
select_entity = next((e for e in entities if e.name == "Test Select"), None)
assert select_entity is not None, "Test Select entity not found"
baud_entity = next(
(e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"),
None,
)
baud_entity = next((e for e in entities if e.name == "Baud Rate"), None)
assert baud_entity is not None, "Baud Rate entity not found"
action_entity = next((e for e in entities if e.name == "Action Select"), None)
assert action_entity is not None, "Action Select entity not found"
# Change select to Option B - this should trigger on_value with StringRef
client.select_command(select_entity.key, "Option B")
# Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod)
client.select_command(baud_entity.key, "115200")
# Change action select - tests set_action trigger (TemplateSelectWithSetAction)
client.select_command(action_entity.key, "Action B")
# Wait for all log messages confirming StringRef operations work
try:
@@ -118,6 +124,7 @@ async def test_select_stringref_trigger(
find_substr_future,
find_char_future,
substr_future,
set_action_future,
stoi_future,
stol_future,
stof_future,
@@ -135,6 +142,7 @@ async def test_select_stringref_trigger(
"find_substr": find_substr_future.done(),
"find_char": find_char_future.done(),
"substr": substr_future.done(),
"set_action": set_action_future.done(),
"stoi": stoi_future.done(),
"stol": stol_future.done(),
"stof": stof_future.done(),