mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c2c850cb4 | ||
|
|
95e4837b99 | ||
|
|
16f14317a7 | ||
|
|
08387b0fb0 | ||
|
|
e18413c2b7 | ||
|
|
086bfba8b3 | ||
|
|
92e8a64669 | ||
|
|
32bde52dba | ||
|
|
32240fd69f | ||
|
|
8aa327264a | ||
|
|
b9a5ccea65 | ||
|
|
aee3476828 | ||
|
|
602c751c2f |
No files matched your search
+2
-47
@@ -5,7 +5,6 @@ import logging
|
||||
import math
|
||||
import re
|
||||
import string
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -2124,17 +2123,6 @@ def _direct_function(
|
||||
)
|
||||
|
||||
|
||||
def _call_target(
|
||||
plan: list[MockObj | list[_InlineAction]],
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
) -> MockObj | str:
|
||||
"""What a generated callback calls: the direct function, else ``<automation>->trigger``."""
|
||||
if (target := _direct_function(plan, args, config)) is None:
|
||||
target = parent_ref(_automation_from_plan(plan, args, config)).trigger
|
||||
return target
|
||||
|
||||
|
||||
def _when_call(
|
||||
when: str | ApplyCall | None, parent: MockObj | None
|
||||
) -> ApplyCall | None:
|
||||
@@ -2152,40 +2140,6 @@ async def build_automation(
|
||||
return await new_automation(args, config, trigger)
|
||||
|
||||
|
||||
async def build_component_automation(
|
||||
config: ConfigType, component_class: MockObjClass, priority: float | None = None
|
||||
) -> MockObj:
|
||||
"""Create and register the Component that runs a no-argument automation.
|
||||
|
||||
That is ``component_class<function[, priority bits]>``, whose function runs the actions
|
||||
directly when they are all inline, else triggers their Automation. A ``trigger_id`` set
|
||||
by hand keeps its Trigger, built with ``priority`` if given.
|
||||
"""
|
||||
trigger_id: ID = config[CONF_TRIGGER_ID]
|
||||
if trigger_id.is_manual:
|
||||
var = cg.new_Pvariable(trigger_id, *([] if priority is None else [priority]))
|
||||
await cg.register_component(var, config)
|
||||
await build_automation(var, [], config)
|
||||
return var
|
||||
# Declared and registered before the actions are built, which can wait for other ids
|
||||
name = _apply_function_name(trigger_id)
|
||||
define = cg.declare_static_function(name, cg.void, [])
|
||||
id_ = trigger_id.copy()
|
||||
id_.type = component_class
|
||||
bits = [] if priority is None else [_float_bits(priority)]
|
||||
var = cg.new_Pvariable(id_, cg.TemplateArguments(cg.RawExpression(name), *bits))
|
||||
await cg.register_component(var, config)
|
||||
plan = await _build_automation_plan([], config)
|
||||
define([str(cg.statement(CallExpression(_call_target(plan, [], config))))])
|
||||
return var
|
||||
|
||||
|
||||
def _float_bits(value: float) -> HexInt:
|
||||
"""``value`` as a C++ float's bit pattern, for a template argument."""
|
||||
(bits,) = struct.unpack("<I", struct.pack("<f", value))
|
||||
return HexInt(bits)
|
||||
|
||||
|
||||
async def build_trigger_callback(
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
@@ -2219,7 +2173,8 @@ async def _trigger_callback(
|
||||
call: ApplyCall | None,
|
||||
parent: MockObj | None,
|
||||
) -> LambdaExpression:
|
||||
target = _call_target(plan, args, config)
|
||||
if (target := _direct_function(plan, args, config)) is None:
|
||||
target = f"{parent_ref(_automation_from_plan(plan, args, config))}->trigger"
|
||||
statements: list[str] = []
|
||||
if call is not None:
|
||||
members = call.members
|
||||
|
||||
@@ -31,7 +31,6 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
add_global,
|
||||
add_library,
|
||||
add_platformio_option,
|
||||
declare_static_function,
|
||||
extern_progmem_array,
|
||||
get_variable,
|
||||
get_variable_with_full_id,
|
||||
|
||||
@@ -144,6 +144,7 @@ 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"
|
||||
|
||||
@@ -453,6 +454,9 @@ 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:
|
||||
@@ -706,21 +710,34 @@ async def to_code(config: ConfigType) -> None:
|
||||
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
|
||||
await wizard.to_code(wizard_config)
|
||||
|
||||
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],
|
||||
)
|
||||
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 (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
|
||||
if key := encryption_config.get(CONF_KEY):
|
||||
|
||||
@@ -251,6 +251,9 @@ 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;
|
||||
@@ -415,10 +418,25 @@ 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);
|
||||
}
|
||||
@@ -1936,8 +1954,8 @@ void APIConnection::complete_authentication_() {
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
{
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
||||
std::string(this->helper_->get_peername_to(peername)));
|
||||
this->parent_->get_client_connected_callback().call(StringRef(this->helper_->get_client_name()),
|
||||
StringRef(this->helper_->get_peername_to(peername)));
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
|
||||
@@ -296,7 +296,11 @@ 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) {
|
||||
if (this->active_iterator_ == ActiveIterator::NONE
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
|| this->active_iterator_ == ActiveIterator::CLIENT_SYNCED_PENDING
|
||||
#endif
|
||||
) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
@@ -642,7 +646,15 @@ 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 };
|
||||
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
|
||||
};
|
||||
|
||||
union IteratorUnion {
|
||||
ListEntitiesIterator list_entities;
|
||||
|
||||
@@ -257,9 +257,11 @@ 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 peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
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);
|
||||
#endif
|
||||
|
||||
// Read before the swap-and-reset below destroys the connection
|
||||
@@ -302,7 +304,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_trigger_.trigger(client_name, client_peername);
|
||||
this->client_disconnected_callback_.call(StringRef(client_name), StringRef(peername));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -277,11 +277,27 @@ 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
|
||||
Trigger<std::string, std::string> *get_client_connected_trigger() { return &this->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_; }
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *get_client_disconnected_trigger() { return &this->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_; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
@@ -343,10 +359,13 @@ class APIServer final : public Component
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
socket::ListenSocket *socket_{nullptr};
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> client_connected_trigger_;
|
||||
ClientCallback client_connected_callback_;
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> client_disconnected_trigger_;
|
||||
ClientCallback client_disconnected_callback_;
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|
||||
ClientCallback client_synced_callback_;
|
||||
#endif
|
||||
|
||||
// 4-byte aligned types
|
||||
|
||||
@@ -171,41 +171,6 @@ class LoopTrigger : public Trigger<>, public Component {
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
};
|
||||
|
||||
/// Component whose setup priority is the float with bit pattern PRIORITY_BITS, since GCC 10 (ESP8266)
|
||||
/// has no float template parameters. Templated on the priority only, so each priority has one copy.
|
||||
template<uint32_t PRIORITY_BITS> class PriorityComponent : public Component {
|
||||
public:
|
||||
float get_setup_priority() const override { return bit_cast<float>(PRIORITY_BITS); }
|
||||
};
|
||||
|
||||
/// Bit pattern of setup_priority::DATA (600.0f), the Component default.
|
||||
static constexpr uint32_t SETUP_PRIORITY_DATA_BITS = 0x44160000;
|
||||
// bit_cast is constexpr only where std::bit_cast exists, which GCC 10 (ESP8266) lacks
|
||||
#if __cpp_lib_bit_cast >= 201806
|
||||
static_assert(bit_cast<uint32_t>(setup_priority::DATA) == SETUP_PRIORITY_DATA_BITS);
|
||||
#endif
|
||||
|
||||
/// The default priority needs no override.
|
||||
template<> class PriorityComponent<SETUP_PRIORITY_DATA_BITS> : public Component {};
|
||||
|
||||
/// Calls Fn from setup(), for on_boot.
|
||||
template<auto Fn, uint32_t PRIORITY_BITS> class StartupCallback final : public PriorityComponent<PRIORITY_BITS> {
|
||||
public:
|
||||
void setup() override { Fn(); }
|
||||
};
|
||||
|
||||
/// Calls Fn from on_shutdown(), for on_shutdown.
|
||||
template<auto Fn, uint32_t PRIORITY_BITS> class ShutdownCallback final : public PriorityComponent<PRIORITY_BITS> {
|
||||
public:
|
||||
void on_shutdown() override { Fn(); }
|
||||
};
|
||||
|
||||
/// Calls Fn from loop(), for on_loop.
|
||||
template<auto Fn> class LoopCallback final : public Component {
|
||||
public:
|
||||
void loop() override { Fn(); }
|
||||
};
|
||||
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
class ProjectUpdateTrigger : public Trigger<std::string>, public Component {
|
||||
public:
|
||||
|
||||
+14
-12
@@ -124,9 +124,6 @@ ShutdownTrigger = cg.esphome_ns.class_(
|
||||
LoopTrigger = cg.esphome_ns.class_(
|
||||
"LoopTrigger", cg.Component, automation.Trigger.template()
|
||||
)
|
||||
StartupCallback = cg.esphome_ns.class_("StartupCallback", cg.Component)
|
||||
ShutdownCallback = cg.esphome_ns.class_("ShutdownCallback", cg.Component)
|
||||
LoopCallback = cg.esphome_ns.class_("LoopCallback", cg.Component)
|
||||
ProjectUpdateTrigger = cg.esphome_ns.class_(
|
||||
"ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string)
|
||||
)
|
||||
@@ -788,15 +785,20 @@ async def _add_environment_variables(env_vars: dict[str, str]) -> None:
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _add_automations(config):
|
||||
for key, component_class in (
|
||||
(CONF_ON_BOOT, StartupCallback),
|
||||
(CONF_ON_SHUTDOWN, ShutdownCallback),
|
||||
(CONF_ON_LOOP, LoopCallback),
|
||||
):
|
||||
for conf in config.get(key, []):
|
||||
await automation.build_component_automation(
|
||||
conf, component_class, conf.get(CONF_PRIORITY)
|
||||
)
|
||||
for conf in config.get(CONF_ON_BOOT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_SHUTDOWN, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_LOOP, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
|
||||
# Datetime component has special subtypes that need additional defines
|
||||
|
||||
@@ -401,6 +401,7 @@
|
||||
#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
|
||||
|
||||
@@ -883,13 +883,14 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
|
||||
return Pvariable(id_, rhs)
|
||||
|
||||
|
||||
def declare_static_function(
|
||||
def static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
) -> Callable[[list[str | Statement]], RawExpression]:
|
||||
"""Declare ``static <return_type> <name>(parameters)`` now and return the function that
|
||||
emits its body, for a caller that names the function before the body is known.
|
||||
body: list[str | Statement],
|
||||
) -> RawExpression:
|
||||
"""Emit ``static <return_type> <name>(parameters) { body }`` at global scope and return an
|
||||
expression naming it, for use as a template argument or a function pointer.
|
||||
|
||||
The declaration goes with the other globals, so a template argument can name the function
|
||||
at once; the body goes after the user includes, where every id and everything those
|
||||
@@ -899,24 +900,10 @@ def declare_static_function(
|
||||
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
|
||||
)
|
||||
add_global(RawStatement(f"{signature};"))
|
||||
|
||||
def define(body: list[str | Statement]) -> RawExpression:
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
|
||||
return define
|
||||
|
||||
|
||||
def static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
body: list[str | Statement],
|
||||
) -> RawExpression:
|
||||
"""``declare_static_function`` with the body known at once."""
|
||||
return declare_static_function(name, return_type, parameters)(body)
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -30,6 +30,14 @@ def _automation_of(main_cpp: str, trigger: str) -> str:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _has_delay(main_cpp: str, trigger: str) -> bool:
|
||||
automation = _automation_of(main_cpp, trigger)
|
||||
return (
|
||||
re.search(rf"{automation}->add_action\(delayaction_id\w*\);", main_cpp)
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def _registered(main_cpp: str) -> list[str]:
|
||||
return re.findall(r"App\.register_component_\((\w+), \d+\);", main_cpp)
|
||||
|
||||
@@ -48,59 +56,18 @@ def test_core_automations_registration_order(main_cpp: str) -> None:
|
||||
]
|
||||
|
||||
|
||||
def _function_body(main_cpp: str, name: str) -> str:
|
||||
match = re.search(rf"static void {name}\(\) \{{(.*?)\n\}}\n", main_cpp, re.DOTALL)
|
||||
assert match is not None, name
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _actions_body(main_cpp: str, function: str) -> str:
|
||||
"""The body of the function of inline actions that ``function`` calls."""
|
||||
match = re.fullmatch(r"\n (\w+)\(\);", _function_body(main_cpp, function))
|
||||
assert match is not None, function
|
||||
return _function_body(main_cpp, match.group(1))
|
||||
|
||||
|
||||
def _delayed_automation(main_cpp: str, function: str) -> str:
|
||||
"""The Automation that ``function`` triggers, checked to hold a delay; found through the
|
||||
function rather than an id counter."""
|
||||
match = re.fullmatch(
|
||||
r"\n ::(\w+)->trigger\(\);", _function_body(main_cpp, function)
|
||||
)
|
||||
assert match is not None, function
|
||||
automation = match.group(1)
|
||||
assert f"new({automation}) Automation<>();" in main_cpp
|
||||
assert re.search(rf"{automation}->add_action\(delayaction_id\w*\);", main_cpp)
|
||||
return automation
|
||||
|
||||
|
||||
def test_on_boot_inline_priorities(main_cpp: str) -> None:
|
||||
"""All-inline on_boot blocks call one function from setup() at their priority, default 600.
|
||||
|
||||
The priority is the float's bit pattern: 0x44480000 is 800.0f, 0x44160000 is 600.0f.
|
||||
"""
|
||||
assert _constructed(main_cpp, "startuptrigger_id") == (
|
||||
"StartupCallback<esphome__startuptrigger_id__fn, 0x44480000>()"
|
||||
)
|
||||
assert _constructed(main_cpp, "startuptrigger_id_2") == (
|
||||
"StartupCallback<esphome__startuptrigger_id_2__fn, 0x44160000>()"
|
||||
)
|
||||
assert "Automation<>(startuptrigger_id" not in main_cpp
|
||||
# Both actions of the first block run in the one function, with no stop checks
|
||||
body = _actions_body(main_cpp, "esphome__startuptrigger_id__fn")
|
||||
assert "boot high" in body
|
||||
assert "boot high lambda" in body
|
||||
assert "esphome__running" not in body
|
||||
assert "Automation" not in body
|
||||
assert "boot default" in _actions_body(main_cpp, "esphome__startuptrigger_id_2__fn")
|
||||
"""All-inline on_boot blocks keep their configured priority, default 600."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id") == "StartupTrigger(800.0f)"
|
||||
assert _constructed(main_cpp, "startuptrigger_id_2") == "StartupTrigger(600.0f)"
|
||||
_automation_of(main_cpp, "startuptrigger_id")
|
||||
_automation_of(main_cpp, "startuptrigger_id_2")
|
||||
|
||||
|
||||
def test_on_boot_delay_triggers_automation(main_cpp: str) -> None:
|
||||
"""A delay keeps the Automation of separate actions, which the function triggers."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id_3") == (
|
||||
"StartupCallback<esphome__startuptrigger_id_3__fn, 0xC2C80000>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__startuptrigger_id_3__fn")
|
||||
def test_on_boot_delay_keeps_trigger(main_cpp: str) -> None:
|
||||
"""A delay needs the Trigger and its Automation of separate actions."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id_3") == "StartupTrigger(-100.0f)"
|
||||
assert _has_delay(main_cpp, "startuptrigger_id_3")
|
||||
|
||||
|
||||
def test_on_boot_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
|
||||
@@ -110,30 +77,19 @@ def test_on_boot_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
|
||||
|
||||
|
||||
def test_on_shutdown_priorities(main_cpp: str) -> None:
|
||||
"""on_shutdown calls its function at its priority; a delay keeps the Automation."""
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id") == (
|
||||
"ShutdownCallback<esphome__shutdowntrigger_id__fn, 0x442F0000>()"
|
||||
)
|
||||
assert "shutdown inline" in _actions_body(
|
||||
main_cpp, "esphome__shutdowntrigger_id__fn"
|
||||
)
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id_2") == (
|
||||
"ShutdownCallback<esphome__shutdowntrigger_id_2__fn, 0x44160000>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__shutdowntrigger_id_2__fn")
|
||||
"""on_shutdown blocks keep their priority; a delay keeps the Trigger."""
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id") == "ShutdownTrigger(700.0f)"
|
||||
_automation_of(main_cpp, "shutdowntrigger_id")
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id_2") == "ShutdownTrigger(600.0f)"
|
||||
assert _has_delay(main_cpp, "shutdowntrigger_id_2")
|
||||
|
||||
|
||||
def test_on_loop(main_cpp: str) -> None:
|
||||
"""on_loop calls its function each loop; a delay keeps the Automation."""
|
||||
assert _constructed(main_cpp, "looptrigger_id") == (
|
||||
"LoopCallback<esphome__looptrigger_id__fn>()"
|
||||
)
|
||||
assert "loop inline" in _actions_body(main_cpp, "esphome__looptrigger_id__fn")
|
||||
assert "HasLoopOverride<LoopCallback<esphome__looptrigger_id__fn>>" in main_cpp
|
||||
assert _constructed(main_cpp, "looptrigger_id_2") == (
|
||||
"LoopCallback<esphome__looptrigger_id_2__fn>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__looptrigger_id_2__fn")
|
||||
"""on_loop blocks are looping components; a delay keeps the Trigger."""
|
||||
assert _constructed(main_cpp, "looptrigger_id") == "LoopTrigger()"
|
||||
_automation_of(main_cpp, "looptrigger_id")
|
||||
assert _constructed(main_cpp, "looptrigger_id_2") == "LoopTrigger()"
|
||||
assert _has_delay(main_cpp, "looptrigger_id_2")
|
||||
|
||||
|
||||
def test_on_boot_registered_before_its_actions_are_built(
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
|
||||
logger:
|
||||
|
||||
interval:
|
||||
- interval: 1s
|
||||
then:
|
||||
- logger.log: inline tick
|
||||
- lambda: ESP_LOGD("test", "inline lambda");
|
||||
- interval: 2s
|
||||
startup_delay: 5s
|
||||
then:
|
||||
- logger.log: delayed start tick
|
||||
- interval: 3s
|
||||
then:
|
||||
- logger.log: before delay
|
||||
- delay: 10ms
|
||||
- logger.log: after delay
|
||||
- id: named_interval
|
||||
interval: 4s
|
||||
startup_delay: 3s
|
||||
then:
|
||||
- logger.log: named tick
|
||||
- interval: 5s
|
||||
setup_priority: 250
|
||||
then:
|
||||
# The script is declared later, so building this action waits for it
|
||||
- script.execute: later_script
|
||||
|
||||
script:
|
||||
- id: later_script
|
||||
then:
|
||||
- logger.log: script ran
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: some_sensor
|
||||
lambda: return 1.0;
|
||||
@@ -1,72 +0,0 @@
|
||||
"""Tests for the code generated for interval blocks."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
CONFIG = "interval_codegen.yaml"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_cpp(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> str:
|
||||
return generate_main(component_config_path(CONFIG))
|
||||
|
||||
|
||||
def _constructed(main_cpp: str, var: str) -> str:
|
||||
match = re.search(rf"new\({var}\) ([^;]+);", main_cpp)
|
||||
assert match is not None, var
|
||||
return match.group(1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("var", "expected", "update_interval"),
|
||||
[
|
||||
("interval_intervaltrigger_id", "interval::IntervalTrigger()", 1000),
|
||||
(
|
||||
"interval_intervaltrigger_id_2",
|
||||
"interval::DelayedIntervalTrigger(5000)",
|
||||
2000,
|
||||
),
|
||||
("interval_intervaltrigger_id_3", "interval::IntervalTrigger()", 3000),
|
||||
("named_interval", "interval::DelayedIntervalTrigger(3000)", 4000),
|
||||
("interval_intervaltrigger_id_4", "interval::IntervalTrigger()", 5000),
|
||||
],
|
||||
)
|
||||
def test_interval_constructed(
|
||||
main_cpp: str, var: str, expected: str, update_interval: int
|
||||
) -> None:
|
||||
"""Each interval is built with its startup delay, if any, and gets its update interval."""
|
||||
assert _constructed(main_cpp, var) == expected
|
||||
assert f"{var}->set_update_interval({update_interval});" in main_cpp
|
||||
|
||||
|
||||
def test_interval_delay_keeps_automation(main_cpp: str) -> None:
|
||||
"""A delay needs the Automation of separate actions."""
|
||||
automation = re.search(
|
||||
r"new\((\w+)\) Automation<>\(interval_intervaltrigger_id_3\);", main_cpp
|
||||
)
|
||||
assert automation is not None
|
||||
assert re.search(
|
||||
rf"{automation.group(1)}->add_action\(delayaction_id\w*\);", main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_interval_registration_order(main_cpp: str) -> None:
|
||||
"""Intervals register in config order; one waiting for a later id keeps its place."""
|
||||
registered = re.findall(r"App\.register_component_\((\w+), \d+\);", main_cpp)
|
||||
assert registered[-6:] == [
|
||||
"interval_intervaltrigger_id",
|
||||
"interval_intervaltrigger_id_2",
|
||||
"interval_intervaltrigger_id_3",
|
||||
"named_interval",
|
||||
"interval_intervaltrigger_id_4",
|
||||
"some_sensor",
|
||||
]
|
||||
assert main_cpp.index(
|
||||
"App.register_component_(interval_intervaltrigger_id_4"
|
||||
) < main_cpp.index("new(later_script)")
|
||||
@@ -98,6 +98,16 @@ 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,64 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/base_automation.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
int calls = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void count_call() { calls++; }
|
||||
|
||||
// Bit patterns of 800.0f, 600.0f, -100.0f and 250.5f, as the code generator writes them
|
||||
constexpr uint32_t PRIORITY_800 = 0x44480000;
|
||||
constexpr uint32_t PRIORITY_MINUS_100 = 0xC2C80000;
|
||||
constexpr uint32_t PRIORITY_250_5 = 0x437A8000;
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CoreAutomationCallbacks, StartupCallbackRunsFromSetupAtItsPriority) {
|
||||
calls = 0;
|
||||
StartupCallback<count_call, PRIORITY_800> high;
|
||||
StartupCallback<count_call, PRIORITY_MINUS_100> late;
|
||||
StartupCallback<count_call, PRIORITY_250_5> fractional;
|
||||
EXPECT_EQ(high.get_setup_priority(), 800.0f);
|
||||
EXPECT_EQ(late.get_setup_priority(), -100.0f);
|
||||
EXPECT_EQ(fractional.get_setup_priority(), 250.5f);
|
||||
StartupCallback<count_call, SETUP_PRIORITY_DATA_BITS> standard;
|
||||
EXPECT_EQ(standard.get_setup_priority(), setup_priority::DATA);
|
||||
|
||||
high.loop();
|
||||
high.on_shutdown();
|
||||
EXPECT_EQ(calls, 0);
|
||||
high.setup();
|
||||
EXPECT_EQ(calls, 1);
|
||||
}
|
||||
|
||||
TEST(CoreAutomationCallbacks, ShutdownCallbackRunsFromOnShutdownAtItsPriority) {
|
||||
calls = 0;
|
||||
ShutdownCallback<count_call, PRIORITY_MINUS_100> shutdown;
|
||||
EXPECT_EQ(shutdown.get_setup_priority(), -100.0f);
|
||||
|
||||
shutdown.setup();
|
||||
shutdown.loop();
|
||||
EXPECT_EQ(calls, 0);
|
||||
shutdown.on_shutdown();
|
||||
EXPECT_EQ(calls, 1);
|
||||
}
|
||||
|
||||
TEST(CoreAutomationCallbacks, LoopCallbackRunsEachLoopAtDataPriority) {
|
||||
calls = 0;
|
||||
LoopCallback<count_call> looping;
|
||||
EXPECT_EQ(looping.get_setup_priority(), setup_priority::DATA);
|
||||
EXPECT_TRUE(HasLoopOverride<LoopCallback<count_call>>::value);
|
||||
EXPECT_FALSE((HasLoopOverride<StartupCallback<count_call, PRIORITY_800>>::value));
|
||||
|
||||
looping.setup();
|
||||
EXPECT_EQ(calls, 0);
|
||||
looping.loop();
|
||||
looping.loop();
|
||||
EXPECT_EQ(calls, 2);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -1,34 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/interval/interval.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::interval::testing {
|
||||
|
||||
class CountAction : public Action<> {
|
||||
public:
|
||||
explicit CountAction(int *count) : count_(count) {}
|
||||
void play() override { (*this->count_)++; }
|
||||
|
||||
protected:
|
||||
int *count_;
|
||||
};
|
||||
|
||||
TEST(IntervalTrigger, UpdateRunsTheAutomationAtDataPriority) {
|
||||
IntervalTrigger trigger;
|
||||
Automation<> automation{&trigger};
|
||||
int count = 0;
|
||||
CountAction action{&count};
|
||||
automation.add_action(&action);
|
||||
|
||||
EXPECT_EQ(trigger.get_setup_priority(), setup_priority::DATA);
|
||||
EXPECT_FALSE(HasLoopOverride<IntervalTrigger>::value);
|
||||
trigger.set_update_interval(1000);
|
||||
EXPECT_EQ(trigger.get_update_interval(), 1000u);
|
||||
|
||||
trigger.update();
|
||||
trigger.update();
|
||||
EXPECT_EQ(count, 2);
|
||||
}
|
||||
|
||||
} // namespace esphome::interval::testing
|
||||
@@ -0,0 +1,28 @@
|
||||
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,46 +0,0 @@
|
||||
esphome:
|
||||
name: interval-runs
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
globals:
|
||||
- id: inline_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
- id: action_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
- id: startup_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
|
||||
interval:
|
||||
# All actions inline
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: id(inline_count) += 1;
|
||||
- lambda: |-
|
||||
if (id(inline_count) == 1) {
|
||||
ESP_LOGI("test", "INLINE first uptime=%u", (unsigned) millis());
|
||||
} else if (id(inline_count) == 20) {
|
||||
ESP_LOGI("test", "INLINE ran 20 times");
|
||||
}
|
||||
# A delay between the actions
|
||||
- interval: 100ms
|
||||
then:
|
||||
- lambda: |-
|
||||
id(action_count) += 1;
|
||||
ESP_LOGI("test", "ACTION start %d", id(action_count));
|
||||
- delay: 20ms
|
||||
- lambda: ESP_LOGI("test", "ACTION end %d", id(action_count));
|
||||
# First run waits for the startup delay
|
||||
- interval: 50ms
|
||||
startup_delay: 1000ms
|
||||
then:
|
||||
- lambda: |-
|
||||
if (++id(startup_count) == 1) {
|
||||
ESP_LOGI("test", "STARTUP first uptime=%u inline_count=%d", (unsigned) millis(), id(inline_count));
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"""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
|
||||
@@ -1,44 +0,0 @@
|
||||
"""Integration test that intervals run repeatedly and honour their startup delay."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
from .types import RunCompiledFunction
|
||||
|
||||
INLINE_FIRST = re.compile(r"INLINE first uptime=(\d+)")
|
||||
STARTUP_FIRST = re.compile(r"STARTUP first uptime=(\d+) inline_count=(\d+)")
|
||||
ACTION = re.compile(r"ACTION (start|end) (\d+)")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interval_runs(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
) -> None:
|
||||
"""Inline and delayed actions repeat; the startup delay holds the first run back."""
|
||||
waiter = LineWaiter()
|
||||
inline_future = waiter.expect(INLINE_FIRST)
|
||||
startup_future = waiter.expect(STARTUP_FIRST)
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=waiter.callback):
|
||||
inline = await waiter.wait(inline_future)
|
||||
startup = await waiter.wait(startup_future)
|
||||
await waiter.wait_for_each("INLINE ran 20 times", "ACTION end 3")
|
||||
|
||||
# The startup delay held the first run back while the inline interval kept running
|
||||
assert int(startup.group(1)) - int(inline.group(1)) >= 800
|
||||
assert int(startup.group(2)) >= 10
|
||||
# Each run of the delayed actions finished before the next started
|
||||
actions = [m.groups() for line in waiter.lines if (m := ACTION.search(line))]
|
||||
assert actions[:6] == [
|
||||
("start", "1"),
|
||||
("end", "1"),
|
||||
("start", "2"),
|
||||
("end", "2"),
|
||||
("start", "3"),
|
||||
("end", "3"),
|
||||
]
|
||||
@@ -1173,21 +1173,6 @@ def test_static_function_declares_early_and_defines_after_includes() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_declare_static_function_defines_the_body_later() -> None:
|
||||
"""The declaration is emitted at once; the body only when define() is called."""
|
||||
define = cg.declare_static_function("esphome__act__fn", ct.void, [])
|
||||
assert [str(st) for st in CORE.global_statements] == [
|
||||
"static void esphome__act__fn();"
|
||||
]
|
||||
assert CORE.global_statements_after_includes == []
|
||||
|
||||
fn = define(["first();", "second();"])
|
||||
assert str(fn) == "esphome__act__fn"
|
||||
assert [str(st) for st in CORE.global_statements_after_includes] == [
|
||||
"static void esphome__act__fn() {\n first();\n second();\n}"
|
||||
]
|
||||
|
||||
|
||||
def test_static_function_accepts_statements() -> None:
|
||||
call = cg.MockObj("foo")(cg.MockObj("x") >> 2)
|
||||
cg.static_function(
|
||||
|
||||
Reference in new issue
Block a user