1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-18 07:45:56 +00:00

Merge dev branch with action continuation optimizations

- Integrated upstream loop re-entry fixes from PR #7972
- Updated WhileAction and RepeatAction to use simpler parameter passing (no var_ storage)
- Maintained all optimization benefits (ContinuationAction, WhileLoopContinuation, RepeatLoopContinuation)
- DelayAction: shared_ptr + lambda instead of std::bind
- WaitUntilAction: simple lambda instead of std::bind
- IfAction: ContinuationAction (4-8 bytes) instead of LambdaAction (40 bytes)
- WhileAction: WhileLoopContinuation with simplified parameter passing
- RepeatAction: RepeatLoopContinuation with simplified parameter passing
This commit is contained in:
J. Nick Koston
2025-11-02 17:06:22 -06:00
37 changed files with 1037 additions and 330 deletions

View File

@@ -434,8 +434,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
return APIError::OK; return APIError::OK;
} }
std::vector<uint8_t> *raw_buffer = buffer.get_buffer(); uint8_t *buffer_data = buffer.get_buffer()->data();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear(); this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size()); this->reusable_iovs_.reserve(packets.size());

View File

@@ -230,8 +230,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
return APIError::OK; return APIError::OK;
} }
std::vector<uint8_t> *raw_buffer = buffer.get_buffer(); uint8_t *buffer_data = buffer.get_buffer()->data();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear(); this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size()); this->reusable_iovs_.reserve(packets.size());

View File

@@ -96,7 +96,11 @@ void loop_task(void *pv_params) {
extern "C" void app_main() { extern "C" void app_main() {
esp32::setup_preferences(); esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE
xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle); xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
#else
xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1);
#endif
} }
#endif // USE_ESP_IDF #endif // USE_ESP_IDF

View File

@@ -39,6 +39,7 @@ CONFIG_SCHEMA = (
# due to hardware limitations or lack of reliable interrupt support. This ensures # due to hardware limitations or lack of reliable interrupt support. This ensures
# stable operation on these platforms. Future maintainers should verify platform # stable operation on these platforms. Future maintainers should verify platform
# capabilities before changing this default behavior. # capabilities before changing this default behavior.
# nrf52 has no gpio interrupts implemented yet
cv.SplitDefault( cv.SplitDefault(
CONF_USE_INTERRUPT, CONF_USE_INTERRUPT,
bk72xx=False, bk72xx=False,
@@ -46,7 +47,7 @@ CONFIG_SCHEMA = (
esp8266=True, esp8266=True,
host=True, host=True,
ln882x=False, ln882x=False,
nrf52=True, nrf52=False,
rp2040=True, rp2040=True,
rtl87xx=False, rtl87xx=False,
): cv.boolean, ): cv.boolean,

View File

@@ -58,7 +58,7 @@ from .types import (
FontEngine, FontEngine,
IdleTrigger, IdleTrigger,
ObjUpdateAction, ObjUpdateAction,
PauseTrigger, PlainTrigger,
lv_font_t, lv_font_t,
lv_group_t, lv_group_t,
lv_style_t, lv_style_t,
@@ -151,6 +151,13 @@ for w_type in WIDGET_TYPES.values():
create_modify_schema(w_type), create_modify_schema(w_type),
)(update_to_code) )(update_to_code)
SIMPLE_TRIGGERS = (
df.CONF_ON_PAUSE,
df.CONF_ON_RESUME,
df.CONF_ON_DRAW_START,
df.CONF_ON_DRAW_END,
)
def as_macro(macro, value): def as_macro(macro, value):
if value is None: if value is None:
@@ -244,9 +251,9 @@ def final_validation(configs):
for w in refreshed_widgets: for w in refreshed_widgets:
path = global_config.get_path_for_id(w) path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1]) widget_conf = global_config.get_config_for_path(path[:-1])
if not any(isinstance(v, Lambda) for v in widget_conf.values()): if not any(isinstance(v, (Lambda, dict)) for v in widget_conf.values()):
raise cv.Invalid( raise cv.Invalid(
f"Widget '{w}' does not have any templated properties to refresh", f"Widget '{w}' does not have any dynamic properties to refresh",
) )
@@ -366,16 +373,16 @@ async def to_code(configs):
conf[CONF_TRIGGER_ID], lv_component, templ conf[CONF_TRIGGER_ID], lv_component, templ
) )
await build_automation(idle_trigger, [], conf) await build_automation(idle_trigger, [], conf)
for conf in config.get(df.CONF_ON_PAUSE, ()): for trigger_name in SIMPLE_TRIGGERS:
pause_trigger = cg.new_Pvariable( if conf := config.get(trigger_name):
conf[CONF_TRIGGER_ID], lv_component, True trigger_var = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
) await build_automation(trigger_var, [], conf)
await build_automation(pause_trigger, [], conf) cg.add(
for conf in config.get(df.CONF_ON_RESUME, ()): getattr(
resume_trigger = cg.new_Pvariable( lv_component,
conf[CONF_TRIGGER_ID], lv_component, False f"set_{trigger_name.removeprefix('on_')}_trigger",
) )(trigger_var)
await build_automation(resume_trigger, [], conf) )
await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) await add_on_boot_triggers(config.get(CONF_ON_BOOT, ()))
# This must be done after all widgets are created # This must be done after all widgets are created
@@ -443,16 +450,15 @@ LVGL_SCHEMA = cv.All(
), ),
} }
), ),
cv.Optional(df.CONF_ON_PAUSE): validate_automation( **{
{ cv.Optional(x): validate_automation(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), {
} cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlainTrigger),
), },
cv.Optional(df.CONF_ON_RESUME): validate_automation( single=True,
{ )
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), for x in SIMPLE_TRIGGERS
} },
),
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list( cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(
WIDGET_SCHEMA WIDGET_SCHEMA
), ),

View File

@@ -400,7 +400,8 @@ async def obj_refresh_to_code(config, action_id, template_arg, args):
# must pass all widget-specific options here, even if not templated, but only do so if at least one is # must pass all widget-specific options here, even if not templated, but only do so if at least one is
# templated. First filter out common style properties. # templated. First filter out common style properties.
config = {k: v for k, v in widget.config.items() if k not in ALL_STYLES} config = {k: v for k, v in widget.config.items() if k not in ALL_STYLES}
if any(isinstance(v, Lambda) for v in config.values()): # Check if v is a Lambda or a dict, implying it is dynamic
if any(isinstance(v, (Lambda, dict)) for v in config.values()):
await widget.type.to_code(widget, config) await widget.type.to_code(widget, config)
if ( if (
widget.type.w_type.value_property is not None widget.type.w_type.value_property is not None

View File

@@ -31,7 +31,7 @@ async def to_code(config):
lvgl_static.add_event_cb( lvgl_static.add_event_cb(
widget.obj, widget.obj,
await pressed_ctx.get_lambda(), await pressed_ctx.get_lambda(),
LV_EVENT.PRESSING, LV_EVENT.PRESSED,
LV_EVENT.RELEASED, LV_EVENT.RELEASED,
) )
) )

View File

@@ -483,6 +483,8 @@ CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj" CONF_OBJ = "obj"
CONF_ONE_CHECKED = "one_checked" CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line" CONF_ONE_LINE = "one_line"
CONF_ON_DRAW_START = "on_draw_start"
CONF_ON_DRAW_END = "on_draw_end"
CONF_ON_PAUSE = "on_pause" CONF_ON_PAUSE = "on_pause"
CONF_ON_RESUME = "on_resume" CONF_ON_RESUME = "on_resume"
CONF_ON_SELECT = "on_select" CONF_ON_SELECT = "on_select"

View File

@@ -82,6 +82,18 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1; area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1;
} }
void LvglComponent::monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px) {
ESP_LOGVV(TAG, "Draw end: %" PRIu32 " pixels in %" PRIu32 " ms", px, time);
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
comp->draw_end_();
}
void LvglComponent::render_start_cb(lv_disp_drv_t *disp_drv) {
ESP_LOGVV(TAG, "Draw start");
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
comp->draw_start_();
}
lv_event_code_t lv_api_event; // NOLINT lv_event_code_t lv_api_event; // NOLINT
lv_event_code_t lv_update_event; // NOLINT lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() { void LvglComponent::dump_config() {
@@ -101,7 +113,10 @@ void LvglComponent::set_paused(bool paused, bool show_snow) {
lv_disp_trig_activity(this->disp_); // resets the inactivity time lv_disp_trig_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_scr_act()); lv_obj_invalidate(lv_scr_act());
} }
this->pause_callbacks_.call(paused); if (paused && this->pause_callback_ != nullptr)
this->pause_callback_->trigger();
if (!paused && this->resume_callback_ != nullptr)
this->resume_callback_->trigger();
} }
void LvglComponent::esphome_lvgl_init() { void LvglComponent::esphome_lvgl_init() {
@@ -225,13 +240,6 @@ IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeo
}); });
} }
PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue<bool> paused) : paused_(std::move(paused)) {
parent->add_on_pause_callback([this](bool pausing) {
if (this->paused_.value() == pausing)
this->trigger();
});
}
#ifdef USE_LVGL_TOUCHSCREEN #ifdef USE_LVGL_TOUCHSCREEN
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) { LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) {
this->set_parent(parent); this->set_parent(parent);
@@ -474,6 +482,12 @@ void LvglComponent::setup() {
return; return;
} }
} }
if (this->draw_start_callback_ != nullptr) {
this->disp_drv_.render_start_cb = render_start_cb;
}
if (this->draw_end_callback_ != nullptr) {
this->disp_drv_.monitor_cb = monitor_cb;
}
#if LV_USE_LOG #if LV_USE_LOG
lv_log_register_print_cb([](const char *buf) { lv_log_register_print_cb([](const char *buf) {
auto next = strchr(buf, ')'); auto next = strchr(buf, ')');
@@ -502,8 +516,9 @@ void LvglComponent::loop() {
if (this->paused_) { if (this->paused_) {
if (this->show_snow_) if (this->show_snow_)
this->write_random_(); this->write_random_();
} else {
lv_timer_handler_run_in_period(5);
} }
lv_timer_handler_run_in_period(5);
} }
#ifdef USE_LVGL_ANIMIMG #ifdef USE_LVGL_ANIMIMG

View File

@@ -171,7 +171,9 @@ class LvglComponent : public PollingComponent {
void add_on_idle_callback(std::function<void(uint32_t)> &&callback) { void add_on_idle_callback(std::function<void(uint32_t)> &&callback) {
this->idle_callbacks_.add(std::move(callback)); this->idle_callbacks_.add(std::move(callback));
} }
void add_on_pause_callback(std::function<void(bool)> &&callback) { this->pause_callbacks_.add(std::move(callback)); }
static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px);
static void render_start_cb(lv_disp_drv_t *disp_drv);
void dump_config() override; void dump_config() override;
bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; }
lv_disp_t *get_disp() { return this->disp_; } lv_disp_t *get_disp() { return this->disp_; }
@@ -213,12 +215,20 @@ class LvglComponent : public PollingComponent {
size_t draw_rounding{2}; size_t draw_rounding{2};
display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES}; display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES};
void set_pause_trigger(Trigger<> *trigger) { this->pause_callback_ = trigger; }
void set_resume_trigger(Trigger<> *trigger) { this->resume_callback_ = trigger; }
void set_draw_start_trigger(Trigger<> *trigger) { this->draw_start_callback_ = trigger; }
void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; }
protected: protected:
// these functions are never called unless the callbacks are non-null since the
// LVGL callbacks that call them are not set unless the start/end callbacks are non-null
void draw_start_() const { this->draw_start_callback_->trigger(); }
void draw_end_() const { this->draw_end_callback_->trigger(); }
void write_random_(); void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); void draw_buffer_(const lv_area_t *area, lv_color_t *ptr);
void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
std::vector<display::Display *> displays_{}; std::vector<display::Display *> displays_{};
size_t buffer_frac_{1}; size_t buffer_frac_{1};
bool full_refresh_{}; bool full_refresh_{};
@@ -235,7 +245,10 @@ class LvglComponent : public PollingComponent {
std::map<lv_group_t *, lv_obj_t *> focus_marks_{}; std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
CallbackManager<void(uint32_t)> idle_callbacks_{}; CallbackManager<void(uint32_t)> idle_callbacks_{};
CallbackManager<void(bool)> pause_callbacks_{}; Trigger<> *pause_callback_{};
Trigger<> *resume_callback_{};
Trigger<> *draw_start_callback_{};
Trigger<> *draw_end_callback_{};
lv_color_t *rotate_buf_{}; lv_color_t *rotate_buf_{};
}; };
@@ -248,14 +261,6 @@ class IdleTrigger : public Trigger<> {
bool is_idle_{}; bool is_idle_{};
}; };
class PauseTrigger : public Trigger<> {
public:
explicit PauseTrigger(LvglComponent *parent, TemplatableValue<bool> paused);
protected:
TemplatableValue<bool> paused_;
};
template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> { template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> {
public: public:
explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {} explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {}

View File

@@ -3,6 +3,7 @@ import sys
from esphome import automation, codegen as cg from esphome import automation, codegen as cg
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
from esphome.cpp_generator import MockObj, MockObjClass from esphome.cpp_generator import MockObj, MockObjClass
from esphome.cpp_types import esphome_ns
from .defines import lvgl_ns from .defines import lvgl_ns
from .lvcode import lv_expr from .lvcode import lv_expr
@@ -42,8 +43,11 @@ lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
lv_key_t = cg.global_ns.enum("lv_key_t") lv_key_t = cg.global_ns.enum("lv_key_t")
FontEngine = lvgl_ns.class_("FontEngine") FontEngine = lvgl_ns.class_("FontEngine")
PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template())
DrawEndTrigger = esphome_ns.class_(
"Trigger<uint32_t, uint32_t>", automation.Trigger.template(cg.uint32, cg.uint32)
)
IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template())
PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template())
ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action)
LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition)
LvglAction = lvgl_ns.class_("LvglAction", automation.Action) LvglAction = lvgl_ns.class_("LvglAction", automation.Action)

View File

@@ -55,6 +55,7 @@ CONFIG_SCHEMA = cv.Schema(
esp32=False, esp32=False,
rp2040=False, rp2040=False,
bk72xx=False, bk72xx=False,
host=False,
): cv.All( ): cv.All(
cv.boolean, cv.boolean,
cv.Any( cv.Any(
@@ -64,6 +65,7 @@ CONFIG_SCHEMA = cv.Schema(
esp8266_arduino=cv.Version(0, 0, 0), esp8266_arduino=cv.Version(0, 0, 0),
rp2040_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0),
bk72xx_arduino=cv.Version(1, 7, 0), bk72xx_arduino=cv.Version(1, 7, 0),
host=cv.Version(0, 0, 0),
), ),
cv.boolean_false, cv.boolean_false,
), ),

View File

@@ -323,6 +323,8 @@ void Nextion::loop() {
this->set_touch_sleep_timeout(this->touch_sleep_timeout_); this->set_touch_sleep_timeout(this->touch_sleep_timeout_);
} }
this->set_auto_wake_on_touch(this->connection_state_.auto_wake_on_touch_);
this->connection_state_.ignore_is_setup_ = false; this->connection_state_.ignore_is_setup_ = false;
} }

View File

@@ -290,6 +290,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
address = ble_device.address address = ble_device.address
else: else:
return True return True
if is_mac_address(address): if is_mac_address(address):
asyncio.run(logger_connect(address)) asyncio.run(logger_connect(address))
return True return True

View File

@@ -33,19 +33,13 @@ Message Format:
class ABBWelcomeData { class ABBWelcomeData {
public: public:
// Make default // Make default
ABBWelcomeData() { ABBWelcomeData() : data_{0x55, 0xff} {}
std::fill(std::begin(this->data_), std::end(this->data_), 0);
this->data_[0] = 0x55;
this->data_[1] = 0xff;
}
// Make from initializer_list // Make from initializer_list
ABBWelcomeData(std::initializer_list<uint8_t> data) { ABBWelcomeData(std::initializer_list<uint8_t> data) : data_{} {
std::fill(std::begin(this->data_), std::end(this->data_), 0);
std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin());
} }
// Make from vector // Make from vector
ABBWelcomeData(const std::vector<uint8_t> &data) { ABBWelcomeData(const std::vector<uint8_t> &data) : data_{} {
std::fill(std::begin(this->data_), std::end(this->data_), 0);
std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin());
} }
// Default copy constructor // Default copy constructor

View File

@@ -2,6 +2,7 @@
#include <memory> #include <memory>
#include <tuple> #include <tuple>
#include <forward_list>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -264,10 +265,22 @@ template<class C, typename... Ts> class IsRunningCondition : public Condition<Ts
C *parent_; C *parent_;
}; };
/** Wait for a script to finish before continuing.
*
* Uses queue-based storage to safely handle concurrent executions.
* While concurrent execution from the same trigger is uncommon, it's possible
* (e.g., rapid button presses, high-frequency sensor updates), so we use
* queue-based storage for correctness.
*/
template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>, public Component { template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>, public Component {
public: public:
ScriptWaitAction(C *script) : script_(script) {} ScriptWaitAction(C *script) : script_(script) {}
void setup() override {
// Start with loop disabled - only enable when there's work to do
this->disable_loop();
}
void play_complex(Ts... x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
// Check if we can continue immediately. // Check if we can continue immediately.
@@ -275,7 +288,11 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
this->play_next_(x...); this->play_next_(x...);
return; return;
} }
this->var_ = std::make_tuple(x...);
// Store parameters for later execution
this->param_queue_.emplace_front(x...);
// Enable loop now that we have work to do
this->enable_loop();
this->loop(); this->loop();
} }
@@ -286,15 +303,30 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
if (this->script_->is_running()) if (this->script_->is_running())
return; return;
this->play_next_tuple_(this->var_); while (!this->param_queue_.empty()) {
auto &params = this->param_queue_.front();
this->play_next_tuple_(params, typename gens<sizeof...(Ts)>::type());
this->param_queue_.pop_front();
}
// Queue is now empty - disable loop until next play_complex
this->disable_loop();
} }
void play(Ts... x) override { /* ignore - see play_complex */ void play(Ts... x) override { /* ignore - see play_complex */
} }
void stop() override {
this->param_queue_.clear();
this->disable_loop();
}
protected: protected:
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->play_next_(std::get<S>(tuple)...);
}
C *script_; C *script_;
std::tuple<Ts...> var_{}; std::forward_list<std::tuple<Ts...>> param_queue_;
}; };
} // namespace script } // namespace script

View File

@@ -12,241 +12,256 @@ CODEOWNERS = ["@bdm310"]
STATE_ARG = "state" STATE_ARG = "state"
SDL_KEYMAP = { SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
"SDLK_UNKNOWN": 0,
"SDLK_FIRST": 0, SDL_KEYS = (
"SDLK_BACKSPACE": 8, "SDLK_UNKNOWN",
"SDLK_TAB": 9, "SDLK_RETURN",
"SDLK_CLEAR": 12, "SDLK_ESCAPE",
"SDLK_RETURN": 13, "SDLK_BACKSPACE",
"SDLK_PAUSE": 19, "SDLK_TAB",
"SDLK_ESCAPE": 27, "SDLK_SPACE",
"SDLK_SPACE": 32, "SDLK_EXCLAIM",
"SDLK_EXCLAIM": 33, "SDLK_QUOTEDBL",
"SDLK_QUOTEDBL": 34, "SDLK_HASH",
"SDLK_HASH": 35, "SDLK_PERCENT",
"SDLK_DOLLAR": 36, "SDLK_DOLLAR",
"SDLK_AMPERSAND": 38, "SDLK_AMPERSAND",
"SDLK_QUOTE": 39, "SDLK_QUOTE",
"SDLK_LEFTPAREN": 40, "SDLK_LEFTPAREN",
"SDLK_RIGHTPAREN": 41, "SDLK_RIGHTPAREN",
"SDLK_ASTERISK": 42, "SDLK_ASTERISK",
"SDLK_PLUS": 43, "SDLK_PLUS",
"SDLK_COMMA": 44, "SDLK_COMMA",
"SDLK_MINUS": 45, "SDLK_MINUS",
"SDLK_PERIOD": 46, "SDLK_PERIOD",
"SDLK_SLASH": 47, "SDLK_SLASH",
"SDLK_0": 48, "SDLK_0",
"SDLK_1": 49, "SDLK_1",
"SDLK_2": 50, "SDLK_2",
"SDLK_3": 51, "SDLK_3",
"SDLK_4": 52, "SDLK_4",
"SDLK_5": 53, "SDLK_5",
"SDLK_6": 54, "SDLK_6",
"SDLK_7": 55, "SDLK_7",
"SDLK_8": 56, "SDLK_8",
"SDLK_9": 57, "SDLK_9",
"SDLK_COLON": 58, "SDLK_COLON",
"SDLK_SEMICOLON": 59, "SDLK_SEMICOLON",
"SDLK_LESS": 60, "SDLK_LESS",
"SDLK_EQUALS": 61, "SDLK_EQUALS",
"SDLK_GREATER": 62, "SDLK_GREATER",
"SDLK_QUESTION": 63, "SDLK_QUESTION",
"SDLK_AT": 64, "SDLK_AT",
"SDLK_LEFTBRACKET": 91, "SDLK_LEFTBRACKET",
"SDLK_BACKSLASH": 92, "SDLK_BACKSLASH",
"SDLK_RIGHTBRACKET": 93, "SDLK_RIGHTBRACKET",
"SDLK_CARET": 94, "SDLK_CARET",
"SDLK_UNDERSCORE": 95, "SDLK_UNDERSCORE",
"SDLK_BACKQUOTE": 96, "SDLK_BACKQUOTE",
"SDLK_a": 97, "SDLK_a",
"SDLK_b": 98, "SDLK_b",
"SDLK_c": 99, "SDLK_c",
"SDLK_d": 100, "SDLK_d",
"SDLK_e": 101, "SDLK_e",
"SDLK_f": 102, "SDLK_f",
"SDLK_g": 103, "SDLK_g",
"SDLK_h": 104, "SDLK_h",
"SDLK_i": 105, "SDLK_i",
"SDLK_j": 106, "SDLK_j",
"SDLK_k": 107, "SDLK_k",
"SDLK_l": 108, "SDLK_l",
"SDLK_m": 109, "SDLK_m",
"SDLK_n": 110, "SDLK_n",
"SDLK_o": 111, "SDLK_o",
"SDLK_p": 112, "SDLK_p",
"SDLK_q": 113, "SDLK_q",
"SDLK_r": 114, "SDLK_r",
"SDLK_s": 115, "SDLK_s",
"SDLK_t": 116, "SDLK_t",
"SDLK_u": 117, "SDLK_u",
"SDLK_v": 118, "SDLK_v",
"SDLK_w": 119, "SDLK_w",
"SDLK_x": 120, "SDLK_x",
"SDLK_y": 121, "SDLK_y",
"SDLK_z": 122, "SDLK_z",
"SDLK_DELETE": 127, "SDLK_CAPSLOCK",
"SDLK_WORLD_0": 160, "SDLK_F1",
"SDLK_WORLD_1": 161, "SDLK_F2",
"SDLK_WORLD_2": 162, "SDLK_F3",
"SDLK_WORLD_3": 163, "SDLK_F4",
"SDLK_WORLD_4": 164, "SDLK_F5",
"SDLK_WORLD_5": 165, "SDLK_F6",
"SDLK_WORLD_6": 166, "SDLK_F7",
"SDLK_WORLD_7": 167, "SDLK_F8",
"SDLK_WORLD_8": 168, "SDLK_F9",
"SDLK_WORLD_9": 169, "SDLK_F10",
"SDLK_WORLD_10": 170, "SDLK_F11",
"SDLK_WORLD_11": 171, "SDLK_F12",
"SDLK_WORLD_12": 172, "SDLK_PRINTSCREEN",
"SDLK_WORLD_13": 173, "SDLK_SCROLLLOCK",
"SDLK_WORLD_14": 174, "SDLK_PAUSE",
"SDLK_WORLD_15": 175, "SDLK_INSERT",
"SDLK_WORLD_16": 176, "SDLK_HOME",
"SDLK_WORLD_17": 177, "SDLK_PAGEUP",
"SDLK_WORLD_18": 178, "SDLK_DELETE",
"SDLK_WORLD_19": 179, "SDLK_END",
"SDLK_WORLD_20": 180, "SDLK_PAGEDOWN",
"SDLK_WORLD_21": 181, "SDLK_RIGHT",
"SDLK_WORLD_22": 182, "SDLK_LEFT",
"SDLK_WORLD_23": 183, "SDLK_DOWN",
"SDLK_WORLD_24": 184, "SDLK_UP",
"SDLK_WORLD_25": 185, "SDLK_NUMLOCKCLEAR",
"SDLK_WORLD_26": 186, "SDLK_KP_DIVIDE",
"SDLK_WORLD_27": 187, "SDLK_KP_MULTIPLY",
"SDLK_WORLD_28": 188, "SDLK_KP_MINUS",
"SDLK_WORLD_29": 189, "SDLK_KP_PLUS",
"SDLK_WORLD_30": 190, "SDLK_KP_ENTER",
"SDLK_WORLD_31": 191, "SDLK_KP_1",
"SDLK_WORLD_32": 192, "SDLK_KP_2",
"SDLK_WORLD_33": 193, "SDLK_KP_3",
"SDLK_WORLD_34": 194, "SDLK_KP_4",
"SDLK_WORLD_35": 195, "SDLK_KP_5",
"SDLK_WORLD_36": 196, "SDLK_KP_6",
"SDLK_WORLD_37": 197, "SDLK_KP_7",
"SDLK_WORLD_38": 198, "SDLK_KP_8",
"SDLK_WORLD_39": 199, "SDLK_KP_9",
"SDLK_WORLD_40": 200, "SDLK_KP_0",
"SDLK_WORLD_41": 201, "SDLK_KP_PERIOD",
"SDLK_WORLD_42": 202, "SDLK_APPLICATION",
"SDLK_WORLD_43": 203, "SDLK_POWER",
"SDLK_WORLD_44": 204, "SDLK_KP_EQUALS",
"SDLK_WORLD_45": 205, "SDLK_F13",
"SDLK_WORLD_46": 206, "SDLK_F14",
"SDLK_WORLD_47": 207, "SDLK_F15",
"SDLK_WORLD_48": 208, "SDLK_F16",
"SDLK_WORLD_49": 209, "SDLK_F17",
"SDLK_WORLD_50": 210, "SDLK_F18",
"SDLK_WORLD_51": 211, "SDLK_F19",
"SDLK_WORLD_52": 212, "SDLK_F20",
"SDLK_WORLD_53": 213, "SDLK_F21",
"SDLK_WORLD_54": 214, "SDLK_F22",
"SDLK_WORLD_55": 215, "SDLK_F23",
"SDLK_WORLD_56": 216, "SDLK_F24",
"SDLK_WORLD_57": 217, "SDLK_EXECUTE",
"SDLK_WORLD_58": 218, "SDLK_HELP",
"SDLK_WORLD_59": 219, "SDLK_MENU",
"SDLK_WORLD_60": 220, "SDLK_SELECT",
"SDLK_WORLD_61": 221, "SDLK_STOP",
"SDLK_WORLD_62": 222, "SDLK_AGAIN",
"SDLK_WORLD_63": 223, "SDLK_UNDO",
"SDLK_WORLD_64": 224, "SDLK_CUT",
"SDLK_WORLD_65": 225, "SDLK_COPY",
"SDLK_WORLD_66": 226, "SDLK_PASTE",
"SDLK_WORLD_67": 227, "SDLK_FIND",
"SDLK_WORLD_68": 228, "SDLK_MUTE",
"SDLK_WORLD_69": 229, "SDLK_VOLUMEUP",
"SDLK_WORLD_70": 230, "SDLK_VOLUMEDOWN",
"SDLK_WORLD_71": 231, "SDLK_KP_COMMA",
"SDLK_WORLD_72": 232, "SDLK_KP_EQUALSAS400",
"SDLK_WORLD_73": 233, "SDLK_ALTERASE",
"SDLK_WORLD_74": 234, "SDLK_SYSREQ",
"SDLK_WORLD_75": 235, "SDLK_CANCEL",
"SDLK_WORLD_76": 236, "SDLK_CLEAR",
"SDLK_WORLD_77": 237, "SDLK_PRIOR",
"SDLK_WORLD_78": 238, "SDLK_RETURN2",
"SDLK_WORLD_79": 239, "SDLK_SEPARATOR",
"SDLK_WORLD_80": 240, "SDLK_OUT",
"SDLK_WORLD_81": 241, "SDLK_OPER",
"SDLK_WORLD_82": 242, "SDLK_CLEARAGAIN",
"SDLK_WORLD_83": 243, "SDLK_CRSEL",
"SDLK_WORLD_84": 244, "SDLK_EXSEL",
"SDLK_WORLD_85": 245, "SDLK_KP_00",
"SDLK_WORLD_86": 246, "SDLK_KP_000",
"SDLK_WORLD_87": 247, "SDLK_THOUSANDSSEPARATOR",
"SDLK_WORLD_88": 248, "SDLK_DECIMALSEPARATOR",
"SDLK_WORLD_89": 249, "SDLK_CURRENCYUNIT",
"SDLK_WORLD_90": 250, "SDLK_CURRENCYSUBUNIT",
"SDLK_WORLD_91": 251, "SDLK_KP_LEFTPAREN",
"SDLK_WORLD_92": 252, "SDLK_KP_RIGHTPAREN",
"SDLK_WORLD_93": 253, "SDLK_KP_LEFTBRACE",
"SDLK_WORLD_94": 254, "SDLK_KP_RIGHTBRACE",
"SDLK_WORLD_95": 255, "SDLK_KP_TAB",
"SDLK_KP0": 256, "SDLK_KP_BACKSPACE",
"SDLK_KP1": 257, "SDLK_KP_A",
"SDLK_KP2": 258, "SDLK_KP_B",
"SDLK_KP3": 259, "SDLK_KP_C",
"SDLK_KP4": 260, "SDLK_KP_D",
"SDLK_KP5": 261, "SDLK_KP_E",
"SDLK_KP6": 262, "SDLK_KP_F",
"SDLK_KP7": 263, "SDLK_KP_XOR",
"SDLK_KP8": 264, "SDLK_KP_POWER",
"SDLK_KP9": 265, "SDLK_KP_PERCENT",
"SDLK_KP_PERIOD": 266, "SDLK_KP_LESS",
"SDLK_KP_DIVIDE": 267, "SDLK_KP_GREATER",
"SDLK_KP_MULTIPLY": 268, "SDLK_KP_AMPERSAND",
"SDLK_KP_MINUS": 269, "SDLK_KP_DBLAMPERSAND",
"SDLK_KP_PLUS": 270, "SDLK_KP_VERTICALBAR",
"SDLK_KP_ENTER": 271, "SDLK_KP_DBLVERTICALBAR",
"SDLK_KP_EQUALS": 272, "SDLK_KP_COLON",
"SDLK_UP": 273, "SDLK_KP_HASH",
"SDLK_DOWN": 274, "SDLK_KP_SPACE",
"SDLK_RIGHT": 275, "SDLK_KP_AT",
"SDLK_LEFT": 276, "SDLK_KP_EXCLAM",
"SDLK_INSERT": 277, "SDLK_KP_MEMSTORE",
"SDLK_HOME": 278, "SDLK_KP_MEMRECALL",
"SDLK_END": 279, "SDLK_KP_MEMCLEAR",
"SDLK_PAGEUP": 280, "SDLK_KP_MEMADD",
"SDLK_PAGEDOWN": 281, "SDLK_KP_MEMSUBTRACT",
"SDLK_F1": 282, "SDLK_KP_MEMMULTIPLY",
"SDLK_F2": 283, "SDLK_KP_MEMDIVIDE",
"SDLK_F3": 284, "SDLK_KP_PLUSMINUS",
"SDLK_F4": 285, "SDLK_KP_CLEAR",
"SDLK_F5": 286, "SDLK_KP_CLEARENTRY",
"SDLK_F6": 287, "SDLK_KP_BINARY",
"SDLK_F7": 288, "SDLK_KP_OCTAL",
"SDLK_F8": 289, "SDLK_KP_DECIMAL",
"SDLK_F9": 290, "SDLK_KP_HEXADECIMAL",
"SDLK_F10": 291, "SDLK_LCTRL",
"SDLK_F11": 292, "SDLK_LSHIFT",
"SDLK_F12": 293, "SDLK_LALT",
"SDLK_F13": 294, "SDLK_LGUI",
"SDLK_F14": 295, "SDLK_RCTRL",
"SDLK_F15": 296, "SDLK_RSHIFT",
"SDLK_NUMLOCK": 300, "SDLK_RALT",
"SDLK_CAPSLOCK": 301, "SDLK_RGUI",
"SDLK_SCROLLOCK": 302, "SDLK_MODE",
"SDLK_RSHIFT": 303, "SDLK_AUDIONEXT",
"SDLK_LSHIFT": 304, "SDLK_AUDIOPREV",
"SDLK_RCTRL": 305, "SDLK_AUDIOSTOP",
"SDLK_LCTRL": 306, "SDLK_AUDIOPLAY",
"SDLK_RALT": 307, "SDLK_AUDIOMUTE",
"SDLK_LALT": 308, "SDLK_MEDIASELECT",
"SDLK_RMETA": 309, "SDLK_WWW",
"SDLK_LMETA": 310, "SDLK_MAIL",
"SDLK_LSUPER": 311, "SDLK_CALCULATOR",
"SDLK_RSUPER": 312, "SDLK_COMPUTER",
"SDLK_MODE": 313, "SDLK_AC_SEARCH",
"SDLK_COMPOSE": 314, "SDLK_AC_HOME",
"SDLK_HELP": 315, "SDLK_AC_BACK",
"SDLK_PRINT": 316, "SDLK_AC_FORWARD",
"SDLK_SYSREQ": 317, "SDLK_AC_STOP",
"SDLK_BREAK": 318, "SDLK_AC_REFRESH",
"SDLK_MENU": 319, "SDLK_AC_BOOKMARKS",
"SDLK_POWER": 320, "SDLK_BRIGHTNESSDOWN",
"SDLK_EURO": 321, "SDLK_BRIGHTNESSUP",
"SDLK_UNDO": 322, "SDLK_DISPLAYSWITCH",
} "SDLK_KBDILLUMTOGGLE",
"SDLK_KBDILLUMDOWN",
"SDLK_KBDILLUMUP",
"SDLK_EJECT",
"SDLK_SLEEP",
"SDLK_APP1",
"SDLK_APP2",
"SDLK_AUDIOREWIND",
"SDLK_AUDIOFASTFORWARD",
"SDLK_SOFTLEFT",
"SDLK_SOFTRIGHT",
"SDLK_CALL",
"SDLK_ENDCALL",
)
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(BinarySensor) binary_sensor.binary_sensor_schema(BinarySensor)

View File

@@ -3,7 +3,7 @@
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
#include <zephyr/drivers/watchdog.h> #include <zephyr/drivers/watchdog.h>
#include <zephyr/sys/reboot.h> #include <zephyr/sys/reboot.h>
#include <zephyr/random/rand32.h> #include <zephyr/random/random.h>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"

View File

@@ -8,8 +8,8 @@ namespace zephyr {
static const char *const TAG = "zephyr"; static const char *const TAG = "zephyr";
static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) { static gpio_flags_t flags_to_mode(gpio::Flags flags, bool inverted, bool value) {
int ret = 0; gpio_flags_t ret = 0;
if (flags & gpio::FLAG_INPUT) { if (flags & gpio::FLAG_INPUT) {
ret |= GPIO_INPUT; ret |= GPIO_INPUT;
} }
@@ -79,7 +79,10 @@ void ZephyrGPIOPin::pin_mode(gpio::Flags flags) {
if (nullptr == this->gpio_) { if (nullptr == this->gpio_) {
return; return;
} }
gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); auto ret = gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_));
if (ret != 0) {
ESP_LOGE(TAG, "gpio %u cannot be configured %d.", this->pin_, ret);
}
} }
std::string ZephyrGPIOPin::dump_summary() const { std::string ZephyrGPIOPin::dump_summary() const {

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
@@ -18,6 +19,7 @@ import logging
from pathlib import Path from pathlib import Path
import re import re
from string import ascii_letters, digits from string import ascii_letters, digits
import typing
import uuid as uuid_ import uuid as uuid_
import voluptuous as vol import voluptuous as vol
@@ -1763,16 +1765,37 @@ class SplitDefault(Optional):
class OnlyWith(Optional): class OnlyWith(Optional):
"""Set the default value only if the given component is loaded.""" """Set the default value only if the given component(s) is/are loaded.
def __init__(self, key, component, default=None): This validator allows configuration keys to have defaults that are only applied
when specific component(s) are loaded. Supports both single component names and
lists of components.
Args:
key: Configuration key
component: Single component name (str) or list of component names.
For lists, ALL components must be loaded for the default to apply.
default: Default value to use when condition is met
Example:
# Single component
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(MQTTComponent)
# Multiple components (all must be loaded)
cv.OnlyWith(CONF_ZIGBEE_ID, ["zigbee", "nrf52"]): cv.use_id(Zigbee)
"""
def __init__(self, key, component: str | list[str], default=None) -> None:
super().__init__(key) super().__init__(key)
self._component = component self._component = component
self._default = vol.default_factory(default) self._default = vol.default_factory(default)
@property @property
def default(self): def default(self) -> Callable[[], typing.Any] | vol.Undefined:
if self._component in CORE.loaded_integrations: if isinstance(self._component, list):
if all(c in CORE.loaded_integrations for c in self._component):
return self._default
elif self._component in CORE.loaded_integrations:
return self._default return self._default
return vol.UNDEFINED return vol.UNDEFINED

View File

@@ -576,10 +576,11 @@ void Application::yield_with_select_(uint32_t delay_ms) {
// Update fd_set if socket list has changed // Update fd_set if socket list has changed
if (this->socket_fds_changed_) { if (this->socket_fds_changed_) {
FD_ZERO(&this->base_read_fds_); FD_ZERO(&this->base_read_fds_);
// fd bounds are already validated in register_socket_fd() or guaranteed by platform design:
// - ESP32: LwIP guarantees fd < FD_SETSIZE by design (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS)
// - Other platforms: register_socket_fd() validates fd < FD_SETSIZE
for (int fd : this->socket_fds_) { for (int fd : this->socket_fds_) {
if (fd >= 0 && fd < FD_SETSIZE) { FD_SET(fd, &this->base_read_fds_);
FD_SET(fd, &this->base_read_fds_);
}
} }
this->socket_fds_changed_ = false; this->socket_fds_changed_ = false;
} }

View File

@@ -10,6 +10,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <vector> #include <vector>
#include <forward_list>
namespace esphome { namespace esphome {
@@ -315,18 +316,16 @@ template<typename... Ts> class WhileAction : public Action<Ts...> {
void play_complex(Ts... x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
// Store loop parameters
this->var_ = std::make_tuple(x...);
// Initial condition check // Initial condition check
if (!this->condition_->check_tuple(this->var_)) { if (!this->condition_->check(x...)) {
// If new condition check failed, stop loop if running // If new condition check failed, stop loop if running
this->then_.stop(); this->then_.stop();
this->play_next_tuple_(this->var_); this->play_next_(x...);
return; return;
} }
if (this->num_running_ > 0) { if (this->num_running_ > 0) {
this->then_.play_tuple(this->var_); this->then_.play(x...);
} }
} }
@@ -338,19 +337,18 @@ template<typename... Ts> class WhileAction : public Action<Ts...> {
protected: protected:
Condition<Ts...> *condition_; Condition<Ts...> *condition_;
ActionList<Ts...> then_; ActionList<Ts...> then_;
std::tuple<Ts...> var_{};
}; };
// Implementation of WhileLoopContinuation::play // Implementation of WhileLoopContinuation::play
template<typename... Ts> void WhileLoopContinuation<Ts...>::play(Ts... x) { template<typename... Ts> void WhileLoopContinuation<Ts...>::play(Ts... x) {
if (this->parent_->num_running_ > 0 && this->parent_->condition_->check_tuple(this->parent_->var_)) { if (this->parent_->num_running_ > 0 && this->parent_->condition_->check(x...)) {
// play again // play again
if (this->parent_->num_running_ > 0) { if (this->parent_->num_running_ > 0) {
this->parent_->then_.play_tuple(this->parent_->var_); this->parent_->then_.play(x...);
} }
} else { } else {
// condition false, play next // condition false, play next
this->parent_->play_next_tuple_(this->parent_->var_); this->parent_->play_next_(x...);
} }
} }
@@ -382,11 +380,10 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> {
void play_complex(Ts... x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
this->var_ = std::make_tuple(x...);
if (this->count_.value(x...) > 0) { if (this->count_.value(x...) > 0) {
this->then_.play(0, x...); this->then_.play(0, x...);
} else { } else {
this->play_next_tuple_(this->var_); this->play_next_(x...);
} }
} }
@@ -397,14 +394,13 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> {
protected: protected:
ActionList<uint32_t, Ts...> then_; ActionList<uint32_t, Ts...> then_;
std::tuple<Ts...> var_;
}; };
// Implementation of RepeatLoopContinuation::play // Implementation of RepeatLoopContinuation::play
template<typename... Ts> void RepeatLoopContinuation<Ts...>::play(uint32_t iteration, Ts... x) { template<typename... Ts> void RepeatLoopContinuation<Ts...>::play(uint32_t iteration, Ts... x) {
iteration++; iteration++;
if (iteration >= this->parent_->count_.value(x...)) { if (iteration >= this->parent_->count_.value(x...)) {
this->parent_->play_next_tuple_(this->parent_->var_); this->parent_->play_next_(x...);
} else { } else {
this->parent_->then_.play(iteration, x...); this->parent_->then_.play(iteration, x...);
} }

View File

@@ -350,7 +350,7 @@ def safe_exp(obj: SafeExpType) -> Expression:
return IntLiteral(int(obj.total_seconds)) return IntLiteral(int(obj.total_seconds))
if isinstance(obj, TimePeriodMinutes): if isinstance(obj, TimePeriodMinutes):
return IntLiteral(int(obj.total_minutes)) return IntLiteral(int(obj.total_minutes))
if isinstance(obj, tuple | list): if isinstance(obj, (tuple, list)):
return ArrayInitializer(*[safe_exp(o) for o in obj]) return ArrayInitializer(*[safe_exp(o) for o in obj])
if obj is bool: if obj is bool:
return bool_ return bool_

View File

@@ -133,7 +133,6 @@ ignore = [
"PLW1641", # Object does not implement `__hash__` method "PLW1641", # Object does not implement `__hash__` method
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
"UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681
] ]
[tool.ruff.lint.isort] [tool.ruff.lint.isort]

View File

@@ -14,12 +14,14 @@ interval:
// Test parse_json // Test parse_json
bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) { bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) {
if (root.containsKey("sensor") && root.containsKey("value")) { if (root["sensor"].is<const char*>() && root["value"].is<float>()) {
const char* sensor = root["sensor"]; const char* sensor = root["sensor"];
float value = root["value"]; float value = root["value"];
ESP_LOGD("test", "Parsed: sensor=%s, value=%.1f", sensor, value); ESP_LOGD("test", "Parsed: sensor=%s, value=%.1f", sensor, value);
return true;
} else { } else {
ESP_LOGD("test", "Parsed JSON missing required keys"); ESP_LOGD("test", "Parsed JSON missing required keys");
return false;
} }
}); });
ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed"); ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed");

View File

@@ -68,5 +68,13 @@ lvgl:
enter_button: pushbutton enter_button: pushbutton
group: general group: general
initial_focus: lv_roller initial_focus: lv_roller
on_draw_start:
- logger.log: draw started
on_draw_end:
- logger.log: draw ended
- lvgl.pause:
- component.update: tft_display
- delay: 60s
- lvgl.resume:
<<: !include common.yaml <<: !include common.yaml

View File

@@ -0,0 +1,2 @@
network:
enable_ipv6: true

View File

@@ -0,0 +1,3 @@
nrf52:
# it is not correct bootloader for the board
bootloader: adafruit_nrf52_sd140_v6

View File

@@ -14,10 +14,10 @@ display:
binary_sensor: binary_sensor:
- platform: sdl - platform: sdl
id: key_up id: key_up
key: SDLK_a key: SDLK_UP
- platform: sdl - platform: sdl
id: key_down id: key_down
key: SDLK_d key: SDLK_DOWN
- platform: sdl - platform: sdl
id: key_enter id: key_enter
key: SDLK_s key: SDLK_RETURN

View File

@@ -0,0 +1,29 @@
esphome:
name: test-web-server-idf
esp32:
board: esp32dev
framework:
type: esp-idf
network:
# Add some entities to test SSE event formatting
sensor:
- platform: template
name: "Test Sensor"
id: test_sensor
update_interval: 60s
lambda: "return 42.5;"
binary_sensor:
- platform: template
name: "Test Binary Sensor"
id: test_binary_sensor
lambda: "return true;"
switch:
- platform: template
name: "Test Switch"
id: test_switch
optimistic: true

View File

@@ -0,0 +1,3 @@
<<: !include common.yaml
web_server:

View File

@@ -0,0 +1,105 @@
esphome:
name: action-concurrent-reentry
on_boot:
- priority: -100
then:
- repeat:
count: 5
then:
- lambda: id(handler_wait_until)->execute(id(global_counter));
- lambda: id(handler_repeat)->execute(id(global_counter));
- lambda: id(handler_while)->execute(id(global_counter));
- lambda: id(handler_script_wait)->execute(id(global_counter));
- delay: 50ms
- lambda: id(global_counter)++;
- delay: 50ms
host:
api:
globals:
- id: global_counter
type: int
script:
- id: handler_wait_until
mode: parallel
parameters:
arg: int
then:
- wait_until:
condition:
lambda: return id(global_counter) == 5;
- logger.log:
format: "AFTER wait_until ARG %d"
args:
- arg
- id: handler_script_wait
mode: parallel
parameters:
arg: int
then:
- script.wait: handler_wait_until
- logger.log:
format: "AFTER script.wait ARG %d"
args:
- arg
- id: handler_repeat
mode: parallel
parameters:
arg: int
then:
- repeat:
count: 3
then:
- logger.log:
format: "IN repeat %d ARG %d"
args:
- iteration
- arg
- delay: 100ms
- logger.log:
format: "AFTER repeat ARG %d"
args:
- arg
- id: handler_while
mode: parallel
parameters:
arg: int
then:
- while:
condition:
lambda: return id(global_counter) != 5;
then:
- logger.log:
format: "IN while ARG %d"
args:
- arg
- delay: 100ms
- logger.log:
format: "AFTER while ARG %d"
args:
- arg
logger:
level: DEBUG

View File

@@ -0,0 +1,130 @@
esphome:
name: test-automation-wait-actions
host:
api:
actions:
# Test 1: Trigger wait_until automation 5 times rapidly
- action: test_wait_until
then:
- logger.log: "=== TEST 1: Triggering wait_until automation 5 times ==="
# Publish 5 different values to trigger the on_value automation 5 times
- sensor.template.publish:
id: wait_until_sensor
state: 1
- sensor.template.publish:
id: wait_until_sensor
state: 2
- sensor.template.publish:
id: wait_until_sensor
state: 3
- sensor.template.publish:
id: wait_until_sensor
state: 4
- sensor.template.publish:
id: wait_until_sensor
state: 5
# Wait then satisfy the condition so all 5 waiting actions complete
- delay: 100ms
- globals.set:
id: test_flag
value: 'true'
# Test 2: Trigger script.wait automation 5 times rapidly
- action: test_script_wait
then:
- logger.log: "=== TEST 2: Triggering script.wait automation 5 times ==="
# Start a long-running script
- script.execute: blocking_script
# Publish 5 different values to trigger the on_value automation 5 times
- sensor.template.publish:
id: script_wait_sensor
state: 1
- sensor.template.publish:
id: script_wait_sensor
state: 2
- sensor.template.publish:
id: script_wait_sensor
state: 3
- sensor.template.publish:
id: script_wait_sensor
state: 4
- sensor.template.publish:
id: script_wait_sensor
state: 5
# Test 3: Trigger wait_until timeout automation 5 times rapidly
- action: test_wait_timeout
then:
- logger.log: "=== TEST 3: Triggering timeout automation 5 times ==="
# Publish 5 different values (condition will never be true, all will timeout)
- sensor.template.publish:
id: timeout_sensor
state: 1
- sensor.template.publish:
id: timeout_sensor
state: 2
- sensor.template.publish:
id: timeout_sensor
state: 3
- sensor.template.publish:
id: timeout_sensor
state: 4
- sensor.template.publish:
id: timeout_sensor
state: 5
logger:
level: DEBUG
globals:
- id: test_flag
type: bool
restore_value: false
initial_value: 'false'
- id: timeout_flag
type: bool
restore_value: false
initial_value: 'false'
# Sensors with wait_until/script.wait in their on_value automations
sensor:
# Test 1: on_value automation with wait_until
- platform: template
id: wait_until_sensor
on_value:
# This wait_until will be hit 5 times before any complete
- wait_until:
condition:
lambda: return id(test_flag);
- logger.log: "wait_until automation completed"
# Test 2: on_value automation with script.wait
- platform: template
id: script_wait_sensor
on_value:
# This script.wait will be hit 5 times before any complete
- script.wait: blocking_script
- logger.log: "script.wait automation completed"
# Test 3: on_value automation with wait_until timeout
- platform: template
id: timeout_sensor
on_value:
# This wait_until will be hit 5 times, all will timeout
- wait_until:
condition:
lambda: return id(timeout_flag);
timeout: 200ms
- logger.log: "timeout automation completed"
script:
# Blocking script for script.wait test
- id: blocking_script
mode: single
then:
- logger.log: "Blocking script: START"
- delay: 200ms
- logger.log: "Blocking script: END"

View File

@@ -0,0 +1,92 @@
"""Integration test for API conditional memory optimization with triggers and services."""
from __future__ import annotations
import asyncio
import collections
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_action_concurrent_reentry(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""
This test runs a script in parallel with varying arguments and verifies if
each script keeps its original argument throughout its execution
"""
test_complete = asyncio.Event()
expected = {0, 1, 2, 3, 4}
# Patterns to match in logs
after_wait_until_pattern = re.compile(r"AFTER wait_until ARG (\d+)")
after_script_wait_pattern = re.compile(r"AFTER script\.wait ARG (\d+)")
after_repeat_pattern = re.compile(r"AFTER repeat ARG (\d+)")
in_repeat_pattern = re.compile(r"IN repeat (\d+) ARG (\d+)")
after_while_pattern = re.compile(r"AFTER while ARG (\d+)")
in_while_pattern = re.compile(r"IN while ARG (\d+)")
after_wait_until_args = []
after_script_wait_args = []
after_while_args = []
in_while_args = []
after_repeat_args = []
in_repeat_args = collections.defaultdict(list)
def check_output(line: str) -> None:
"""Check log output for expected messages."""
if test_complete.is_set():
return
if mo := after_wait_until_pattern.search(line):
after_wait_until_args.append(int(mo.group(1)))
elif mo := after_script_wait_pattern.search(line):
after_script_wait_args.append(int(mo.group(1)))
elif mo := in_while_pattern.search(line):
in_while_args.append(int(mo.group(1)))
elif mo := after_while_pattern.search(line):
after_while_args.append(int(mo.group(1)))
elif mo := in_repeat_pattern.search(line):
in_repeat_args[int(mo.group(1))].append(int(mo.group(2)))
elif mo := after_repeat_pattern.search(line):
after_repeat_args.append(int(mo.group(1)))
if len(after_repeat_args) == len(expected):
test_complete.set()
# Run with log monitoring
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Verify device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "action-concurrent-reentry"
# Wait for tests to complete with timeout
try:
await asyncio.wait_for(test_complete.wait(), timeout=8.0)
except TimeoutError:
pytest.fail("test timed out")
# order may change, but all args must be present
for args in in_repeat_args.values():
assert set(args) == expected
assert set(in_repeat_args.keys()) == {0, 1, 2}
assert set(after_wait_until_args) == expected, after_wait_until_args
assert set(after_script_wait_args) == expected, after_script_wait_args
assert set(after_repeat_args) == expected, after_repeat_args
assert set(after_while_args) == expected, after_while_args
assert dict(collections.Counter(in_while_args)) == {
0: 5,
1: 4,
2: 3,
3: 2,
4: 1,
}, in_while_args

View File

@@ -0,0 +1,104 @@
"""Test concurrent execution of wait_until and script.wait in direct automation actions."""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_automation_wait_actions(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""
Test that wait_until and script.wait correctly handle concurrent executions
when automation actions (not scripts) are triggered multiple times rapidly.
This tests sensor.on_value automations being triggered 5 times before any complete.
"""
loop = asyncio.get_running_loop()
# Track completion counts
test_results = {
"wait_until": 0,
"script_wait": 0,
"wait_until_timeout": 0,
}
# Patterns for log messages
wait_until_complete = re.compile(r"wait_until automation completed")
script_wait_complete = re.compile(r"script\.wait automation completed")
timeout_complete = re.compile(r"timeout automation completed")
# Test completion futures
test1_complete = loop.create_future()
test2_complete = loop.create_future()
test3_complete = loop.create_future()
def check_output(line: str) -> None:
"""Check log output for completion messages."""
# Test 1: wait_until concurrent execution
if wait_until_complete.search(line):
test_results["wait_until"] += 1
if test_results["wait_until"] == 5 and not test1_complete.done():
test1_complete.set_result(True)
# Test 2: script.wait concurrent execution
if script_wait_complete.search(line):
test_results["script_wait"] += 1
if test_results["script_wait"] == 5 and not test2_complete.done():
test2_complete.set_result(True)
# Test 3: wait_until with timeout
if timeout_complete.search(line):
test_results["wait_until_timeout"] += 1
if test_results["wait_until_timeout"] == 5 and not test3_complete.done():
test3_complete.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Get services
_, services = await client.list_entities_services()
# Test 1: wait_until in automation - trigger 5 times rapidly
test_service = next((s for s in services if s.name == "test_wait_until"), None)
assert test_service is not None, "test_wait_until service not found"
client.execute_service(test_service, {})
await asyncio.wait_for(test1_complete, timeout=3.0)
# Verify Test 1: All 5 triggers should complete
assert test_results["wait_until"] == 5, (
f"Test 1: Expected 5 wait_until completions, got {test_results['wait_until']}"
)
# Test 2: script.wait in automation - trigger 5 times rapidly
test_service = next((s for s in services if s.name == "test_script_wait"), None)
assert test_service is not None, "test_script_wait service not found"
client.execute_service(test_service, {})
await asyncio.wait_for(test2_complete, timeout=3.0)
# Verify Test 2: All 5 triggers should complete
assert test_results["script_wait"] == 5, (
f"Test 2: Expected 5 script.wait completions, got {test_results['script_wait']}"
)
# Test 3: wait_until with timeout in automation - trigger 5 times rapidly
test_service = next(
(s for s in services if s.name == "test_wait_timeout"), None
)
assert test_service is not None, "test_wait_timeout service not found"
client.execute_service(test_service, {})
await asyncio.wait_for(test3_complete, timeout=3.0)
# Verify Test 3: All 5 triggers should timeout and complete
assert test_results["wait_until_timeout"] == 5, (
f"Test 3: Expected 5 timeout completions, got {test_results['wait_until_timeout']}"
)

View File

@@ -3,6 +3,7 @@ import string
from hypothesis import example, given from hypothesis import example, given
from hypothesis.strategies import builds, integers, ip_addresses, one_of, text from hypothesis.strategies import builds, integers, ip_addresses, one_of, text
import pytest import pytest
import voluptuous as vol
from esphome import config_validation from esphome import config_validation
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
@@ -301,8 +302,6 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple)
], ],
) )
def test_require_framework_version(framework, platform, message): def test_require_framework_version(framework, platform, message):
import voluptuous as vol
from esphome.const import ( from esphome.const import (
KEY_CORE, KEY_CORE,
KEY_FRAMEWORK_VERSION, KEY_FRAMEWORK_VERSION,
@@ -377,3 +376,129 @@ def test_require_framework_version(framework, platform, message):
config_validation.require_framework_version( config_validation.require_framework_version(
extra_message="test 5", extra_message="test 5",
)("test") )("test")
def test_only_with_single_component_loaded() -> None:
"""Test OnlyWith with single component when component is loaded."""
CORE.loaded_integrations = {"mqtt"}
schema = config_validation.Schema(
{
config_validation.OnlyWith("mqtt_id", "mqtt", default="test_mqtt"): str,
}
)
result = schema({})
assert result.get("mqtt_id") == "test_mqtt"
def test_only_with_single_component_not_loaded() -> None:
"""Test OnlyWith with single component when component is not loaded."""
CORE.loaded_integrations = set()
schema = config_validation.Schema(
{
config_validation.OnlyWith("mqtt_id", "mqtt", default="test_mqtt"): str,
}
)
result = schema({})
assert "mqtt_id" not in result
def test_only_with_list_all_components_loaded() -> None:
"""Test OnlyWith with list when all components are loaded."""
CORE.loaded_integrations = {"zigbee", "nrf52"}
schema = config_validation.Schema(
{
config_validation.OnlyWith(
"zigbee_id", ["zigbee", "nrf52"], default="test_zigbee"
): str,
}
)
result = schema({})
assert result.get("zigbee_id") == "test_zigbee"
def test_only_with_list_partial_components_loaded() -> None:
"""Test OnlyWith with list when only some components are loaded."""
CORE.loaded_integrations = {"zigbee"} # Only zigbee, not nrf52
schema = config_validation.Schema(
{
config_validation.OnlyWith(
"zigbee_id", ["zigbee", "nrf52"], default="test_zigbee"
): str,
}
)
result = schema({})
assert "zigbee_id" not in result
def test_only_with_list_no_components_loaded() -> None:
"""Test OnlyWith with list when no components are loaded."""
CORE.loaded_integrations = set()
schema = config_validation.Schema(
{
config_validation.OnlyWith(
"zigbee_id", ["zigbee", "nrf52"], default="test_zigbee"
): str,
}
)
result = schema({})
assert "zigbee_id" not in result
def test_only_with_list_multiple_components() -> None:
"""Test OnlyWith with list requiring three components."""
CORE.loaded_integrations = {"comp1", "comp2", "comp3"}
schema = config_validation.Schema(
{
config_validation.OnlyWith(
"test_id", ["comp1", "comp2", "comp3"], default="test_value"
): str,
}
)
result = schema({})
assert result.get("test_id") == "test_value"
# Test with one missing
CORE.loaded_integrations = {"comp1", "comp2"}
result = schema({})
assert "test_id" not in result
def test_only_with_empty_list() -> None:
"""Test OnlyWith with empty list (edge case)."""
CORE.loaded_integrations = set()
schema = config_validation.Schema(
{
config_validation.OnlyWith("test_id", [], default="test_value"): str,
}
)
# all([]) returns True, so default should be applied
result = schema({})
assert result.get("test_id") == "test_value"
def test_only_with_user_value_overrides_default() -> None:
"""Test OnlyWith respects user-provided values over defaults."""
CORE.loaded_integrations = {"mqtt"}
schema = config_validation.Schema(
{
config_validation.OnlyWith("mqtt_id", "mqtt", default="default_id"): str,
}
)
result = schema({"mqtt_id": "custom_id"})
assert result.get("mqtt_id") == "custom_id"