Compare commits

..

13 Commits

Author SHA1 Message Date
Clyde Stubbs
0a1fa05c8f Merge branch 'dev' into template_select_trigger 2026-02-03 14:57:02 +11:00
clydebarrow
cb9fbf8970 Fix parameter name; set update_interval to never if no lambda to poll 2026-02-03 14:56:21 +11:00
clydebarrow
5a2774876a Use templates to customise classes 2026-02-03 13:14:09 +11:00
clydebarrow
ede2f205d3 Merge branch 'template_select_trigger' of https://github.com/esphome/esphome into template_select_trigger 2026-02-03 08:33:25 +11:00
Clyde Stubbs
8cf29c40a9 Merge branch 'dev' into template_select_trigger 2026-02-03 08:33:10 +11:00
clydebarrow
0332cbfdd4 Merge branch 'dev' of https://github.com/esphome/esphome into template_select_trigger 2026-02-03 08:32:40 +11:00
J. Nick Koston
89bd9b610e modify in validation instead to avoid copy 2026-02-02 02:48:00 +01:00
J. Nick Koston
9dbcf1447b integration test 2026-02-02 02:45:43 +01:00
J. Nick Koston
6c853cae57 use pattern from sensor filters 2026-02-02 02:40:45 +01:00
J. Nick Koston
48e6efb6aa use pattern from sensor filters 2026-02-02 02:40:30 +01:00
J. Nick Koston
cfc3b3336f fix 2026-02-02 02:35:51 +01:00
J. Nick Koston
9ca394d1e5 not as bad as I was thinking it would be 2026-02-02 02:31:29 +01:00
J. Nick Koston
e62a87afe1 [template] Conditionally compile select set_trigger
Only allocate the set_trigger when set_action is configured.
This saves ~20-24 bytes of heap per template select that doesn't
use set_action.
2026-02-01 20:39:52 +01:00
13 changed files with 197 additions and 197 deletions

View File

@@ -94,29 +94,3 @@ DriverChip(
(0x29, 0x00),
],
)
DriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B",
height=600,
width=1024,
hsync_back_porch=160,
hsync_pulse_width=10,
hsync_front_porch=160,
vsync_back_porch=23,
vsync_pulse_width=1,
vsync_front_porch=12,
pclk_frequency="52MHz",
lane_bit_rate="900Mbps",
no_transform=True,
color_order="RGB",
initsequence=[
(0x80, 0x8B),
(0x81, 0x78),
(0x82, 0x84),
(0x83, 0x88),
(0x84, 0xA8),
(0x85, 0xE3),
(0x86, 0x88),
(0xB2, 0x10),
],
)

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

@@ -3,63 +3,17 @@
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));
}
this->publish_state(index);
}
void TemplateSelect::update() {
if (!this->f_.has_value())
return;
auto val = this->f_();
if (val.has_value()) {
if (!this->has_option(*val)) {
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
return;
}
this->publish_state(*val);
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));
}
}
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

@@ -8,30 +8,83 @@
#include "esphome/core/template_lambda.h"
namespace esphome::template_ {
static const char *const TAG = "template.select";
struct Empty {};
class BaseTemplateSelect : public select::Select, public PollingComponent {};
class TemplateSelect final : public select::Select, public PollingComponent {
void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index,
bool restore_value);
/// 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_template(F &&f) { this->f_.set(std::forward<F>(f)); }
template<typename F> void set_lambda(F &&f) {
if constexpr (HAS_LAMBDA) {
this->f_.set(std::forward<F>(f));
}
}
void setup() override;
void update() override;
void dump_config() override;
void setup() override {
if constexpr (!HAS_LAMBDA) {
size_t index = INITIAL_OPTION_INDEX;
if constexpr (RESTORE_VALUE) {
this->pref_ = this->template make_entity_preference<size_t>();
if (this->pref_.load(&index) && this->has_index(index)) {
esph_log_d(TAG, "State from restore: %s", this->option_at(index));
} else {
index = INITIAL_OPTION_INDEX;
esph_log_d(TAG, "State from initial (no valid stored index): %s", this->option_at(INITIAL_OPTION_INDEX));
}
} else {
esph_log_d(TAG, "State from initial: %s", this->option_at(INITIAL_OPTION_INDEX));
}
this->publish_state(index);
}
}
void update() override {
if constexpr (HAS_LAMBDA) {
auto val = this->f_();
if (val.has_value()) {
if (!this->has_option(*val)) {
esph_log_e(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
return;
}
this->publish_state(*val);
}
}
}
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

@@ -1464,12 +1464,6 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
this->release_scan_results_();
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
// Notify listeners now that state machine has reached STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
this->notify_connect_state_listeners_();
#endif
return;
}
@@ -2189,21 +2183,6 @@ void WiFiComponent::release_scan_results_() {
}
}
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
void WiFiComponent::notify_connect_state_listeners_() {
if (!this->pending_.connect_state)
return;
this->pending_.connect_state = false;
// Get current SSID and BSSID from the WiFi driver
char ssid_buf[SSID_BUFFER_SIZE];
const char *ssid = this->wifi_ssid_to(ssid_buf);
bssid_t bssid = this->wifi_bssid();
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid);
}
}
#endif // USE_WIFI_CONNECT_STATE_LISTENERS
void WiFiComponent::check_roaming_(uint32_t now) {
// Guard: not for hidden networks (may not appear in scan)
const WiFiAP *selected = this->get_selected_sta_();

View File

@@ -632,11 +632,6 @@ class WiFiComponent : public Component {
/// Free scan results memory unless a component needs them
void release_scan_results_();
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
/// Notify connect state listeners (called after state machine reaches STA_CONNECTED)
void notify_connect_state_listeners_();
#endif
#ifdef USE_ESP8266
static void wifi_event_callback(System_Event_t *event);
void wifi_scan_done_callback_(void *arg, STATUS status);
@@ -744,16 +739,6 @@ class WiFiComponent : public Component {
SemaphoreHandle_t high_performance_semaphore_{nullptr};
#endif
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
// Pending listener notifications deferred until state machine reaches appropriate state.
// Listeners are notified after state transitions complete so conditions like
// wifi.connected return correct values in automations.
// Uses bitfields to minimize memory; more flags may be added as needed.
struct {
bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED
} pending_{};
#endif
#ifdef USE_WIFI_CONNECT_TRIGGER
Trigger<> connect_trigger_;
#endif

View File

@@ -500,10 +500,6 @@ const LogString *get_disconnect_reason_str(uint8_t reason) {
}
}
// TODO: This callback runs in ESP8266 system context with limited stack (~2KB).
// All listener notifications should be deferred to wifi_loop_() via pending_ flags
// to avoid stack overflow. Currently only connect_state is deferred; disconnect,
// IP, and scan listeners still run in this context and should be migrated.
void WiFiComponent::wifi_event_callback(System_Event_t *event) {
switch (event->event) {
case EVENT_STAMODE_CONNECTED: {
@@ -516,9 +512,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
#endif
s_sta_connected = true;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
// Defer listener notification until state machine reaches STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
global_wifi_component->pending_.connect_state = true;
for (auto *listener : global_wifi_component->connect_state_listeners_) {
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)

View File

@@ -710,9 +710,6 @@ void WiFiComponent::wifi_loop_() {
delete data; // NOLINT(cppcoreguidelines-owning-memory)
}
}
// Events are processed from queue in main loop context, but listener notifications
// must be deferred until after the state machine transitions (in check_connecting_finished)
// so that conditions like wifi.connected return correct values in automations.
void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
esp_err_t err;
if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) {
@@ -746,9 +743,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
#endif
s_sta_connected = true;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
// Defer listener notification until state machine reaches STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
this->pending_.connect_state = true;
for (auto *listener : this->connect_state_listeners_) {
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)

View File

@@ -423,10 +423,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
}
}
// Process a single event from the queue - runs in main loop context.
// Listener notifications must be deferred until after the state machine transitions
// (in check_connecting_finished) so that conditions like wifi.connected return
// correct values in automations.
// Process a single event from the queue - runs in main loop context
void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
switch (event->event_id) {
case ESPHOME_EVENT_ID_WIFI_READY: {
@@ -459,9 +456,9 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
// This matches ESP32 IDF behavior where s_sta_connected is set but
// wifi_sta_connect_status_() also checks got_ipv4_address_
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
// Defer listener notification until state machine reaches STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
this->pending_.connect_state = true;
for (auto *listener : this->connect_state_listeners_) {
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 set connected state here
#ifdef USE_WIFI_MANUAL_IP

View File

@@ -252,10 +252,6 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
return network::IPAddress(dns_ip);
}
// Pico W uses polling for connection state detection.
// Connect state listener notifications are deferred until after the state machine
// transitions (in check_connecting_finished) so that conditions like wifi.connected
// return correct values in automations.
void WiFiComponent::wifi_loop_() {
// Handle scan completion
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
@@ -282,9 +278,11 @@ void WiFiComponent::wifi_loop_() {
s_sta_was_connected = true;
ESP_LOGV(TAG, "Connected");
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
// Defer listener notification until state machine reaches STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
this->pending_.connect_state = true;
String ssid = WiFi.SSID();
bssid_t bssid = this->wifi_bssid();
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid);
}
#endif
// For static IP configurations, notify IP listeners immediately as the IP is already configured
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)

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(),