mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5829740661 | ||
|
|
fc47adb83f | ||
|
|
ebce86fd9b | ||
|
|
d6a44e1b63 |
No files matched your search
+42
-21
@@ -1,6 +1,7 @@
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
@@ -1151,9 +1152,6 @@ IfAction = cg.esphome_ns.class_("IfAction", Action)
|
||||
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
|
||||
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
|
||||
WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component)
|
||||
UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
|
||||
SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
|
||||
ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
|
||||
Automation = cg.esphome_ns.class_("Automation")
|
||||
TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder")
|
||||
TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder")
|
||||
@@ -1771,25 +1769,44 @@ async def lambda_action_to_code(
|
||||
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
|
||||
|
||||
|
||||
register_simple_action(
|
||||
"component.update",
|
||||
UpdateComponentAction,
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||
synchronous=True,
|
||||
_POLLING_COMPONENT_SCHEMA = maybe_simple_id(
|
||||
{cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}
|
||||
)
|
||||
|
||||
|
||||
register_simple_action(
|
||||
"component.suspend",
|
||||
SuspendComponentAction,
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||
synchronous=True,
|
||||
)
|
||||
async def _polling_component_action(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
calls: list[str],
|
||||
) -> MockObj:
|
||||
"""Run ``calls`` on the component once it is ready, through ``PollingComponent``.
|
||||
|
||||
An if block, not an early return, so actions merged after this one still run.
|
||||
"""
|
||||
parent = await _apply_parent(config)
|
||||
statements = [
|
||||
f"{cg.PollingComponent} &esphome__component = *{parent};",
|
||||
"if (esphome__component.is_ready()) {",
|
||||
*(f" esphome__component.{call};" for call in calls),
|
||||
"}",
|
||||
]
|
||||
return apply_statements_action(action_id, template_arg, args, statements)
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("component.update", "update()"),
|
||||
("component.suspend", "stop_poller()"),
|
||||
):
|
||||
register_action(_name, ApplyAction, _POLLING_COMPONENT_SCHEMA, synchronous=True)(
|
||||
partial(_polling_component_action, calls=[_call])
|
||||
)
|
||||
|
||||
|
||||
@register_action(
|
||||
"component.resume",
|
||||
ResumeComponentAction,
|
||||
ApplyAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
@@ -1806,12 +1823,16 @@ async def component_resume_action_to_code(
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, comp)
|
||||
if CONF_UPDATE_INTERVAL in config:
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
|
||||
cg.add(var.set_update_interval(template_))
|
||||
return var
|
||||
calls: list[str] = []
|
||||
if (interval := config.get(CONF_UPDATE_INTERVAL)) is not None:
|
||||
if isinstance(interval, Lambda):
|
||||
# Built with the trigger's own argument types and always called, so the body
|
||||
# may modify its by-value arguments
|
||||
lamb = await cg.process_lambda(interval, args, return_type=cg.uint32)
|
||||
interval = CallExpression(lamb, *(MockObj(name) for _, name in args))
|
||||
calls.append(f"set_update_interval({cg.safe_exp(interval)})")
|
||||
calls.append("start_poller()")
|
||||
return await _polling_component_action(config, action_id, template_arg, args, calls)
|
||||
|
||||
|
||||
async def build_action(
|
||||
|
||||
@@ -144,7 +144,6 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||
CONF_ON_CLIENT_SYNCED = "on_client_synced"
|
||||
CONF_OUTGOING_CONNECTION = "outgoing_connection"
|
||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||
|
||||
@@ -454,9 +453,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_CLIENT_SYNCED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
# Connection limits to prevent memory exhaustion on resource-constrained devices
|
||||
# Each connection uses ~500-1000 bytes of RAM plus system resources
|
||||
# Platform defaults based on available RAM and network stack implementation:
|
||||
@@ -710,34 +706,21 @@ async def to_code(config: ConfigType) -> None:
|
||||
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
|
||||
await wizard.to_code(wizard_config)
|
||||
|
||||
for conf_key, define, callback_method in (
|
||||
(
|
||||
CONF_ON_CLIENT_CONNECTED,
|
||||
"USE_API_CLIENT_CONNECTED_TRIGGER",
|
||||
"set_on_client_connected_callback",
|
||||
),
|
||||
(
|
||||
CONF_ON_CLIENT_DISCONNECTED,
|
||||
"USE_API_CLIENT_DISCONNECTED_TRIGGER",
|
||||
"set_on_client_disconnected_callback",
|
||||
),
|
||||
(
|
||||
CONF_ON_CLIENT_SYNCED,
|
||||
"USE_API_CLIENT_SYNCED_TRIGGER",
|
||||
"set_on_client_synced_callback",
|
||||
),
|
||||
):
|
||||
if (automation_conf := config.get(conf_key)) is not None:
|
||||
cg.add_define(define)
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
callback_method,
|
||||
automation.owning_args(
|
||||
[(cg.StringRef, "client_info"), (cg.StringRef, "client_address")],
|
||||
automation_conf,
|
||||
),
|
||||
automation_conf,
|
||||
)
|
||||
if CONF_ON_CLIENT_CONNECTED in config:
|
||||
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_client_connected_trigger(),
|
||||
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
|
||||
config[CONF_ON_CLIENT_CONNECTED],
|
||||
)
|
||||
|
||||
if CONF_ON_CLIENT_DISCONNECTED in config:
|
||||
cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_client_disconnected_trigger(),
|
||||
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
|
||||
config[CONF_ON_CLIENT_DISCONNECTED],
|
||||
)
|
||||
|
||||
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
|
||||
if key := encryption_config.get(CONF_KEY):
|
||||
|
||||
@@ -251,9 +251,6 @@ void APIConnection::destroy_active_iterator_() {
|
||||
this->iterator_storage_.initial_state.~InitialStateIterator();
|
||||
break;
|
||||
case ActiveIterator::NONE:
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
case ActiveIterator::CLIENT_SYNCED_PENDING:
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
this->active_iterator_ = ActiveIterator::NONE;
|
||||
@@ -418,25 +415,10 @@ void APIConnection::process_active_iterator_() {
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
}
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
} else if (this->active_iterator_ == ActiveIterator::CLIENT_SYNCED_PENDING) {
|
||||
// Fired after the sync, not on SubscribeStates, so the subscriptions sent with it are active.
|
||||
// Waits for the batch to drain so a direct send from the automation cannot overtake queued states.
|
||||
// next_close: a DisconnectRequest this pass already answered; remove is only set next loop.
|
||||
if (this->deferred_batch_.empty() && !this->flags_.remove && !this->flags_.next_close) {
|
||||
this->active_iterator_ = ActiveIterator::NONE;
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
this->parent_->get_client_synced_callback().call(StringRef(this->helper_->get_client_name()),
|
||||
StringRef(this->helper_->get_peername_to(peername)));
|
||||
}
|
||||
#endif
|
||||
} else { // INITIAL_STATE
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
this->finalize_iterator_sync_();
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
this->active_iterator_ = ActiveIterator::CLIENT_SYNCED_PENDING;
|
||||
#endif
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
@@ -1954,8 +1936,8 @@ void APIConnection::complete_authentication_() {
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
{
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
this->parent_->get_client_connected_callback().call(StringRef(this->helper_->get_client_name()),
|
||||
StringRef(this->helper_->get_peername_to(peername)));
|
||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
||||
std::string(this->helper_->get_peername_to(peername)));
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
|
||||
@@ -296,11 +296,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->flags_.state_subscription = true;
|
||||
// Start initial state iterator only if no iterator is active
|
||||
// If list_entities is running, we'll start initial_state when it completes
|
||||
if (this->active_iterator_ == ActiveIterator::NONE
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
|| this->active_iterator_ == ActiveIterator::CLIENT_SYNCED_PENDING
|
||||
#endif
|
||||
) {
|
||||
if (this->active_iterator_ == ActiveIterator::NONE) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
@@ -646,15 +642,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
|
||||
// These iterators are never active simultaneously - list_entities runs to completion
|
||||
// before initial_state begins, so we use a union with explicit construction/destruction.
|
||||
enum class ActiveIterator : uint8_t {
|
||||
NONE,
|
||||
LIST_ENTITIES,
|
||||
INITIAL_STATE,
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
// Initial states sent, on_client_synced not fired yet; uses no iterator storage
|
||||
CLIENT_SYNCED_PENDING,
|
||||
#endif
|
||||
};
|
||||
enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE };
|
||||
|
||||
union IteratorUnion {
|
||||
ListEntitiesIterator list_entities;
|
||||
|
||||
@@ -257,11 +257,9 @@ void APIServer::remove_client_(uint8_t client_index) {
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char client_name[CLIENT_INFO_NAME_MAX_LEN];
|
||||
strncpy(client_name, client->get_name(), sizeof(client_name) - 1);
|
||||
client_name[sizeof(client_name) - 1] = '\0';
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
client->get_peername_to(peername);
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// Read before the swap-and-reset below destroys the connection
|
||||
@@ -304,7 +302,7 @@ void APIServer::remove_client_(uint8_t client_index) {
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_callback_.call(StringRef(client_name), StringRef(peername));
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -277,27 +277,11 @@ class APIServer final : public Component
|
||||
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
||||
#endif
|
||||
|
||||
#if defined(USE_API_CLIENT_CONNECTED_TRIGGER) || defined(USE_API_CLIENT_DISCONNECTED_TRIGGER) || \
|
||||
defined(USE_API_CLIENT_SYNCED_TRIGGER)
|
||||
/// Called with the client name and address, which are only valid during the call
|
||||
using ClientCallback = Callback<void(StringRef, StringRef)>;
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
template<typename F> void set_on_client_connected_callback(F &&callback) {
|
||||
this->client_connected_callback_ = ClientCallback::create(std::forward<F>(callback));
|
||||
}
|
||||
const ClientCallback &get_client_connected_callback() const { return this->client_connected_callback_; }
|
||||
Trigger<std::string, std::string> *get_client_connected_trigger() { return &this->client_connected_trigger_; }
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
template<typename F> void set_on_client_disconnected_callback(F &&callback) {
|
||||
this->client_disconnected_callback_ = ClientCallback::create(std::forward<F>(callback));
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
template<typename F> void set_on_client_synced_callback(F &&callback) {
|
||||
this->client_synced_callback_ = ClientCallback::create(std::forward<F>(callback));
|
||||
}
|
||||
const ClientCallback &get_client_synced_callback() const { return this->client_synced_callback_; }
|
||||
Trigger<std::string, std::string> *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
@@ -359,13 +343,10 @@ class APIServer final : public Component
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
socket::ListenSocket *socket_{nullptr};
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
ClientCallback client_connected_callback_;
|
||||
Trigger<std::string, std::string> client_connected_trigger_;
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
ClientCallback client_disconnected_callback_;
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
ClientCallback client_synced_callback_;
|
||||
Trigger<std::string, std::string> client_disconnected_trigger_;
|
||||
#endif
|
||||
|
||||
// 4-byte aligned types
|
||||
|
||||
@@ -595,52 +595,4 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
|
||||
uint8_t stop_generation_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class UpdateComponentAction : public Action<Ts...> {
|
||||
public:
|
||||
UpdateComponentAction(PollingComponent *component) : component_(component) {}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (!this->component_->is_ready())
|
||||
return;
|
||||
this->component_->update();
|
||||
}
|
||||
|
||||
protected:
|
||||
PollingComponent *component_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SuspendComponentAction : public Action<Ts...> {
|
||||
public:
|
||||
SuspendComponentAction(PollingComponent *component) : component_(component) {}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (!this->component_->is_ready())
|
||||
return;
|
||||
this->component_->stop_poller();
|
||||
}
|
||||
|
||||
protected:
|
||||
PollingComponent *component_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResumeComponentAction : public Action<Ts...> {
|
||||
public:
|
||||
ResumeComponentAction(PollingComponent *component) : component_(component) {}
|
||||
TEMPLATABLE_VALUE(uint32_t, update_interval)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (!this->component_->is_ready()) {
|
||||
return;
|
||||
}
|
||||
optional<uint32_t> update_interval = this->update_interval_.optional_value(x...);
|
||||
if (update_interval.has_value()) {
|
||||
this->component_->set_update_interval(update_interval.value());
|
||||
}
|
||||
this->component_->start_poller();
|
||||
}
|
||||
|
||||
protected:
|
||||
PollingComponent *component_;
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
@@ -401,7 +401,6 @@
|
||||
#define USE_API
|
||||
#define USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
#define USE_API_CLIENT_SYNCED_TRIGGER
|
||||
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
#define USE_API_HOMEASSISTANT_SERVICES
|
||||
|
||||
@@ -98,16 +98,6 @@ esphome:
|
||||
api:
|
||||
port: 8000
|
||||
reboot_timeout: 0min
|
||||
on_client_connected:
|
||||
# Deferring actions get owned copies of the arguments
|
||||
- delay: 1s
|
||||
- logger.log:
|
||||
format: "Client %s connected from %s"
|
||||
args: [client_info.c_str(), client_address.c_str()]
|
||||
on_client_synced:
|
||||
- logger.log:
|
||||
format: "Client %s synced from %s"
|
||||
args: [client_info.c_str(), client_address.c_str()]
|
||||
actions:
|
||||
- action: hello_world
|
||||
description: Log a greeting
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
esphome:
|
||||
name: api-client-synced
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
on_client_synced:
|
||||
- logger.log: "Synced trigger entered"
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- logger.log: "Synced trigger fired"
|
||||
# Home Assistant subscribes to actions in the same packet as states
|
||||
- homeassistant.event:
|
||||
event: esphome.client_synced
|
||||
|
||||
switch:
|
||||
# A known state at boot, so the test can check it is sent before the trigger
|
||||
- platform: template
|
||||
id: ready_switch
|
||||
name: "Ready switch"
|
||||
optimistic: true
|
||||
restore_mode: ALWAYS_ON
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
@@ -1,56 +0,0 @@
|
||||
"""Integration test for the api on_client_synced trigger."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, HomeassistantServiceCall, SwitchState
|
||||
import pytest
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_client_synced(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""The trigger fires after the initial states, with the action subscription active."""
|
||||
waiter = LineWaiter()
|
||||
calls: asyncio.Queue[HomeassistantServiceCall] = asyncio.Queue()
|
||||
# Everything the client receives, in order
|
||||
received: list[EntityState | HomeassistantServiceCall] = []
|
||||
|
||||
def on_service_call(call: HomeassistantServiceCall) -> None:
|
||||
received.append(call)
|
||||
calls.put_nowait(call)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=waiter.callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await client.device_info()
|
||||
assert waiter.count("Synced trigger entered") == 0
|
||||
|
||||
client.subscribe_home_assistant_states_and_services(
|
||||
on_state=received.append,
|
||||
on_service_call=on_service_call,
|
||||
on_state_sub=lambda *_: None,
|
||||
)
|
||||
await waiter.wait_for_each(
|
||||
"Synced trigger entered", "Synced trigger fired", timeout=5.0
|
||||
)
|
||||
call = await asyncio.wait_for(calls.get(), timeout=5.0)
|
||||
assert call.service == "esphome.client_synced"
|
||||
assert call.is_event
|
||||
# The initial switch state arrives before the trigger's event
|
||||
synced_at = received.index(call)
|
||||
switch_states = [
|
||||
i
|
||||
for i, item in enumerate(received[:synced_at])
|
||||
if isinstance(item, SwitchState)
|
||||
]
|
||||
assert switch_states, received
|
||||
assert received[switch_states[0]].state is True
|
||||
@@ -2441,16 +2441,25 @@ def polling_component() -> None:
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("polling_component")
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
["component.update", "component.suspend", "component.resume"],
|
||||
("name", "call"),
|
||||
[
|
||||
("component.update", "update()"),
|
||||
("component.suspend", "stop_poller()"),
|
||||
("component.resume", "start_poller()"),
|
||||
],
|
||||
)
|
||||
async def test_component_action_codegen(name: str) -> None:
|
||||
"""Each action is an object constructed with the component, with no interval set."""
|
||||
async def test_component_action_codegen(name: str, call: str) -> None:
|
||||
"""The call goes through ``PollingComponent`` and only once the component is ready."""
|
||||
await build_action(_component_action(name), TEMPLATE_ARG, [])
|
||||
|
||||
class_name = str(ACTION_REGISTRY[name].type_id)
|
||||
assert f"new(act) {class_name}<>(comp);" in CORE.cpp_main_section
|
||||
assert "set_update_interval" not in CORE.cpp_main_section
|
||||
assert "new(act) ApplyAction<esphome__act__fn>();" in CORE.cpp_main_section
|
||||
assert (
|
||||
" PollingComponent &esphome__component = *::comp;\n"
|
||||
" if (esphome__component.is_ready()) {\n"
|
||||
f" esphome__component.{call};\n"
|
||||
" }\n"
|
||||
) in CORE.cpp_global_section
|
||||
assert "set_update_interval" not in CORE.cpp_global_section
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -2466,14 +2475,17 @@ async def test_component_resume_constant_interval_codegen() -> None:
|
||||
)
|
||||
|
||||
assert (
|
||||
"act->set_update_interval([]() -> uint32_t {\n return 250;\n});"
|
||||
) in CORE.cpp_main_section
|
||||
" if (esphome__component.is_ready()) {\n"
|
||||
" esphome__component.set_update_interval(250);\n"
|
||||
" esphome__component.start_poller();\n"
|
||||
" }\n"
|
||||
) in CORE.cpp_global_section
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("polling_component")
|
||||
async def test_component_resume_interval_lambda_codegen() -> None:
|
||||
"""A templated interval is a lambda over the trigger args."""
|
||||
"""A templated interval is computed from the trigger args, only once ready."""
|
||||
await build_action(
|
||||
_component_action(
|
||||
"component.resume", {CONF_UPDATE_INTERVAL: Lambda("return x * 1000;")}
|
||||
@@ -2482,27 +2494,91 @@ async def test_component_resume_interval_lambda_codegen() -> None:
|
||||
[(cg.uint32, "x")],
|
||||
)
|
||||
|
||||
main = CORE.cpp_main_section
|
||||
assert "new(act) ResumeComponentAction<uint32_t>(comp);" in main
|
||||
assert (
|
||||
"act->set_update_interval([](uint32_t x) -> uint32_t {\n"
|
||||
"static void esphome__act__fn(const std::remove_cvref_t<uint32_t> & x) {\n"
|
||||
" PollingComponent &esphome__component = *::comp;\n"
|
||||
" if (esphome__component.is_ready()) {\n"
|
||||
" esphome__component.set_update_interval([](uint32_t x) -> uint32_t {\n"
|
||||
" return x * 1000;\n"
|
||||
"});"
|
||||
) in main
|
||||
" }(x));\n"
|
||||
" esphome__component.start_poller();\n"
|
||||
" }\n"
|
||||
) in CORE.cpp_global_section
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("apply_actions")
|
||||
async def test_component_update_not_ready_still_runs_merged_actions() -> None:
|
||||
"""The ready check skips only the update, never the actions merged after it."""
|
||||
cg.new_Pvariable(ID("pcomp", is_declaration=True, type=cg.PollingComponent))
|
||||
update = _component_action("component.update")
|
||||
update["component.update"][CONF_ID] = ID("pcomp", type=cg.PollingComponent)
|
||||
|
||||
result = await build_action_list(
|
||||
[update, _action("my.a", "act_a")], TEMPLATE_ARG, []
|
||||
)
|
||||
|
||||
assert [str(action) for action in result] == ["act"]
|
||||
text = CORE.cpp_global_section
|
||||
assert text.index("esphome__component.update();") < text.index("::comp->do_a();")
|
||||
# The only return is the stop check between the two actions
|
||||
assert text.count("return;") == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("apply_actions")
|
||||
async def test_callback_automation_if_with_component_update_is_direct() -> None:
|
||||
"""An ``if`` whose branches update components becomes plain C++ in the callback."""
|
||||
cg.new_Pvariable(ID("pcomp", is_declaration=True, type=cg.PollingComponent))
|
||||
|
||||
def update(id_: str) -> ConfigType:
|
||||
return {
|
||||
"component.update": {CONF_ID: ID("pcomp", type=cg.PollingComponent)},
|
||||
CONF_TYPE_ID: ID(
|
||||
id_, is_declaration=True, type=ApplyAction, is_manual=False
|
||||
),
|
||||
}
|
||||
|
||||
conf = _automation_conf(
|
||||
{
|
||||
"if": {
|
||||
CONF_CONDITION: _inline_condition(),
|
||||
CONF_THEN: [update("upd_then")],
|
||||
CONF_ELSE: [update("upd_else")],
|
||||
},
|
||||
CONF_TYPE_ID: ID(
|
||||
"act_if", is_declaration=True, type=IfAction, is_manual=False
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
await build_callback_automation(CALLBACK_PARENT, "add_cb", [], conf)
|
||||
|
||||
assert "Action" not in CORE.cpp_main_section
|
||||
assert "Condition" not in CORE.cpp_main_section
|
||||
text = CORE.cpp_global_section
|
||||
assert "if (esphome__condition) {" in text
|
||||
assert text.count("esphome__component.update();") == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("polling_component")
|
||||
@pytest.mark.parametrize("body", ["x *= 1000; return x;", "return x *= 1000;"])
|
||||
async def test_component_resume_interval_lambda_takes_args_by_value(body: str) -> None:
|
||||
"""The user's interval lambda gets the trigger args by value, so it may modify them."""
|
||||
"""The user's interval lambda gets the trigger args by value, so it may modify them.
|
||||
|
||||
It is called rather than reduced, so even a single return modifies only its own copy.
|
||||
"""
|
||||
await build_action(
|
||||
_component_action("component.resume", {CONF_UPDATE_INTERVAL: Lambda(body)}),
|
||||
cg.TemplateArguments(cg.uint32),
|
||||
[(cg.uint32, "x")],
|
||||
)
|
||||
|
||||
assert f"[](uint32_t x) -> uint32_t {{\n {body}\n}}" in CORE.cpp_main_section
|
||||
assert (
|
||||
f"set_update_interval([](uint32_t x) -> uint32_t {{\n {body}\n }}(x));"
|
||||
in CORE.cpp_global_section
|
||||
)
|
||||
|
||||
|
||||
COMPONENT_ID = ID("comp", type=cg.Component)
|
||||
|
||||
Reference in new issue
Block a user