mirror of
https://github.com/esphome/esphome.git
synced 2025-07-19 09:46:37 +00:00
[component] Fix `is_ready
` flag when loop disabled (#9501)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
3f492e3b82
commit
d3342d6a1a
@ -264,6 +264,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
|
||||
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||
bool Component::is_ready() const {
|
||||
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
||||
}
|
||||
bool Component::can_proceed() { return true; }
|
||||
|
@ -1,7 +1,7 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME
|
||||
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL
|
||||
|
||||
CODEOWNERS = ["@esphome/tests"]
|
||||
|
||||
@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon
|
||||
LoopTestISRComponent = loop_test_component_ns.class_(
|
||||
"LoopTestISRComponent", cg.Component
|
||||
)
|
||||
LoopTestUpdateComponent = loop_test_component_ns.class_(
|
||||
"LoopTestUpdateComponent", cg.PollingComponent
|
||||
)
|
||||
|
||||
CONF_DISABLE_AFTER = "disable_after"
|
||||
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
|
||||
CONF_ISR_COMPONENTS = "isr_components"
|
||||
CONF_UPDATE_COMPONENTS = "update_components"
|
||||
CONF_DISABLE_LOOP_AFTER = "disable_loop_after"
|
||||
|
||||
COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_,
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
||||
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
|
||||
cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA),
|
||||
cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list(
|
||||
UPDATE_COMPONENT_CONFIG_SCHEMA
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@ -94,3 +111,12 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(isr_config[CONF_ID])
|
||||
await cg.register_component(var, isr_config)
|
||||
cg.add(var.set_name(isr_config[CONF_NAME]))
|
||||
|
||||
# Create update test components
|
||||
for update_config in config.get(CONF_UPDATE_COMPONENTS, []):
|
||||
var = cg.new_Pvariable(update_config[CONF_ID])
|
||||
await cg.register_component(var, update_config)
|
||||
|
||||
cg.add(var.set_name(update_config[CONF_NAME]))
|
||||
cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER]))
|
||||
cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL]))
|
||||
|
@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() {
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
// LoopTestUpdateComponent implementation
|
||||
void LoopTestUpdateComponent::setup() {
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str());
|
||||
}
|
||||
|
||||
void LoopTestUpdateComponent::loop() {
|
||||
this->loop_count_++;
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_);
|
||||
|
||||
// Disable loop after specified count to test component.update when loop is disabled
|
||||
if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_);
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void LoopTestUpdateComponent::update() {
|
||||
this->update_count_++;
|
||||
// Check if loop is disabled by testing component state
|
||||
bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE;
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(),
|
||||
this->update_count_, loop_disabled ? "YES" : "NO");
|
||||
}
|
||||
|
||||
} // namespace loop_test_component
|
||||
} // namespace esphome
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace loop_test_component {
|
||||
@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> {
|
||||
LoopTestComponent *parent_;
|
||||
};
|
||||
|
||||
// Component with update() method to test component.update action
|
||||
class LoopTestUpdateComponent : public PollingComponent {
|
||||
public:
|
||||
LoopTestUpdateComponent() : PollingComponent(1000) {} // Default 1s update interval
|
||||
|
||||
void set_name(const std::string &name) { this->name_ = name; }
|
||||
void set_disable_loop_after(int count) { this->disable_loop_after_ = count; }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
|
||||
int get_update_count() const { return this->update_count_; }
|
||||
int get_loop_count() const { return this->loop_count_; }
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
std::string name_;
|
||||
int loop_count_{0};
|
||||
int update_count_{0};
|
||||
int disable_loop_after_{0};
|
||||
};
|
||||
|
||||
} // namespace loop_test_component
|
||||
} // namespace esphome
|
||||
|
@ -40,6 +40,13 @@ loop_test_component:
|
||||
- id: isr_test
|
||||
name: "isr_test"
|
||||
|
||||
# Update test component to test component.update when loop is disabled
|
||||
update_components:
|
||||
- id: update_test_component
|
||||
name: "update_test"
|
||||
disable_loop_after: 3 # Disable loop after 3 iterations
|
||||
update_interval: 0.1s # Fast update interval for testing
|
||||
|
||||
# Interval to re-enable the self_disable_10 component after some time
|
||||
interval:
|
||||
- interval: 0.5s
|
||||
@ -51,3 +58,28 @@ interval:
|
||||
- logger.log: "Re-enabling self_disable_10 via service"
|
||||
- loop_test_component.enable:
|
||||
id: self_disable_10
|
||||
|
||||
# Test component.update on a component with disabled loop
|
||||
- interval: 0.1s
|
||||
then:
|
||||
- lambda: |-
|
||||
static bool manual_update_done = false;
|
||||
if (!manual_update_done &&
|
||||
id(update_test_component).get_loop_count() == 3 &&
|
||||
id(update_test_component).get_update_count() >= 3) {
|
||||
ESP_LOGI("main", "Manually calling component.update on update_test_component with disabled loop");
|
||||
manual_update_done = true;
|
||||
}
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
static bool manual_update_triggered = false;
|
||||
if (!manual_update_triggered &&
|
||||
id(update_test_component).get_loop_count() == 3 &&
|
||||
id(update_test_component).get_update_count() >= 3) {
|
||||
manual_update_triggered = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
then:
|
||||
- component.update: update_test_component
|
||||
|
@ -45,11 +45,18 @@ async def test_loop_disable_enable(
|
||||
isr_component_disabled = asyncio.Event()
|
||||
isr_component_re_enabled = asyncio.Event()
|
||||
isr_component_pure_re_enabled = asyncio.Event()
|
||||
# Events for update component testing
|
||||
update_component_loop_disabled = asyncio.Event()
|
||||
update_component_manual_update_called = asyncio.Event()
|
||||
|
||||
# Track loop counts for components
|
||||
self_disable_10_counts: list[int] = []
|
||||
normal_component_counts: list[int] = []
|
||||
isr_component_counts: list[int] = []
|
||||
# Track update component behavior
|
||||
update_component_loop_count = 0
|
||||
update_component_update_count = 0
|
||||
update_component_manual_update_count = 0
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
"""Process each log line from the process output."""
|
||||
@ -59,6 +66,7 @@ async def test_loop_disable_enable(
|
||||
if (
|
||||
"loop_test_component" not in clean_line
|
||||
and "loop_test_isr_component" not in clean_line
|
||||
and "Manually calling component.update" not in clean_line
|
||||
):
|
||||
return
|
||||
|
||||
@ -112,6 +120,23 @@ async def test_loop_disable_enable(
|
||||
elif "Running after pure ISR re-enable!" in clean_line:
|
||||
isr_component_pure_re_enabled.set()
|
||||
|
||||
# Update component events
|
||||
elif "[update_test]" in clean_line:
|
||||
if "LoopTestUpdateComponent loop count:" in clean_line:
|
||||
nonlocal update_component_loop_count
|
||||
update_component_loop_count = int(
|
||||
clean_line.split("LoopTestUpdateComponent loop count: ")[1]
|
||||
)
|
||||
elif "LoopTestUpdateComponent update() called" in clean_line:
|
||||
nonlocal update_component_update_count
|
||||
update_component_update_count += 1
|
||||
if "Manually calling component.update" in " ".join(log_messages[-5:]):
|
||||
nonlocal update_component_manual_update_count
|
||||
update_component_manual_update_count += 1
|
||||
update_component_manual_update_called.set()
|
||||
elif "Disabling loop after" in clean_line:
|
||||
update_component_loop_disabled.set()
|
||||
|
||||
# Write, compile and run the ESPHome device with log callback
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
@ -205,3 +230,28 @@ async def test_loop_disable_enable(
|
||||
assert final_count > 10, (
|
||||
f"Component didn't run after pure ISR enable: got {final_count} counts total"
|
||||
)
|
||||
|
||||
# Test component.update functionality when loop is disabled
|
||||
# Wait for update component to disable its loop
|
||||
try:
|
||||
await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail("Update component did not disable its loop within 3 seconds")
|
||||
|
||||
# Verify it ran exactly 3 loops before disabling
|
||||
assert update_component_loop_count == 3, (
|
||||
f"Expected 3 loop iterations before disable, got {update_component_loop_count}"
|
||||
)
|
||||
|
||||
# Wait for manual component.update to be called
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
update_component_manual_update_called.wait(), timeout=5.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail("Manual component.update was not called within 5 seconds")
|
||||
|
||||
# The key test: verify that manual component.update worked after loop was disabled
|
||||
assert update_component_manual_update_count >= 1, (
|
||||
"component.update did not fire after loop was disabled"
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user