mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 09:36:04 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccdad2ba0d | ||
|
|
f9d0367e2b | ||
|
|
79c1ba1e8f | ||
|
|
c190249188 | ||
|
|
9f50af9d73 | ||
|
|
4342ee1ee7 | ||
|
|
b1065984be | ||
|
|
800e5b034c | ||
|
|
63c7323981 | ||
|
|
19a26d4344 | ||
|
|
915f94e2cc | ||
|
|
981348e2a8 | ||
|
|
1754f895f0 | ||
|
|
72413cf22e | ||
|
|
3c09fea844 | ||
|
|
68d3746d90 | ||
|
|
2c269ec219 |
No files matched your search
+90
-33
@@ -335,25 +335,23 @@ def _is_collecting(id_: ID) -> bool:
|
||||
return collector is not None and collector.action_id is id_
|
||||
|
||||
|
||||
def _collect_action(
|
||||
id_: ID, statements: list[str], emit: Callable[[], MockObj] | None
|
||||
) -> bool:
|
||||
"""Record ``id_`` as inline if it is the action or condition being collected.
|
||||
def _collect_action(action: _InlineAction) -> bool:
|
||||
"""Record ``action`` as inline if it is the action or condition being collected.
|
||||
|
||||
Matching the id leaves the conditions and child actions a builder creates to emit normally.
|
||||
"""
|
||||
if not _is_collecting(id_):
|
||||
if not _is_collecting(action.id_):
|
||||
return False
|
||||
collector = _INLINE_COLLECTOR.get()
|
||||
# Replacing it would silently lose the statements already collected
|
||||
if collector.action is not None:
|
||||
raise EsphomeError(f"The builder of {collector.name} built its action twice")
|
||||
collector.action = _InlineAction(id_, statements, emit)
|
||||
collector.action = action
|
||||
return True
|
||||
|
||||
|
||||
def _collect_control_flow(action_id: ID, statements: list[str]) -> MockObj:
|
||||
_collect_action(action_id, statements, None)
|
||||
_collect_action(_InlineAction(action_id, statements, None))
|
||||
return _COLLECTED_ACTION
|
||||
|
||||
|
||||
@@ -409,8 +407,7 @@ def _condition_expression(
|
||||
) -> str:
|
||||
if isinstance(condition, _InlineCondition):
|
||||
return f"({condition.expression})"
|
||||
names = ", ".join(name for _, name in args)
|
||||
return f"{parent_ref(condition)}->check({names})"
|
||||
return f"{parent_ref(condition)}->check({_arg_names(args)})"
|
||||
|
||||
|
||||
def _condition_object(condition: MockObj | _InlineCondition) -> MockObj:
|
||||
@@ -675,12 +672,22 @@ def _apply_function(
|
||||
) -> MockObj:
|
||||
"""Emit the generated function and declare ``id_`` as the ``ApplyAction`` or
|
||||
``ApplyCondition`` templated on it, so ``play()`` calls it directly."""
|
||||
action = _inline_action(id_, return_type, template_arg, args, statements)
|
||||
return _COLLECTED_ACTION if _collect_action(action) else action.emit()
|
||||
|
||||
|
||||
def _inline_action(
|
||||
id_: ID,
|
||||
return_type: SafeExpType,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
statements: list[str],
|
||||
) -> _InlineAction:
|
||||
def emit() -> MockObj:
|
||||
fn = apply_static_function(id_, return_type, _apply_params(args), statements)
|
||||
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
|
||||
|
||||
return _COLLECTED_ACTION if _collect_action(id_, statements, emit) else emit()
|
||||
return _InlineAction(id_, statements, emit)
|
||||
|
||||
|
||||
def apply_statements_action(
|
||||
@@ -1192,11 +1199,13 @@ def new_lambda_pvariable(
|
||||
or stateless_class is StatelessLambdaCondition
|
||||
)
|
||||
and _collect_action(
|
||||
id_obj,
|
||||
[f"{immediate_lambda_call(lambda_expr)};"],
|
||||
lambda: new_lambda_pvariable(
|
||||
id_obj, lambda_expr, stateless_class, template_arg
|
||||
),
|
||||
_InlineAction(
|
||||
id_obj,
|
||||
[f"{immediate_lambda_call(lambda_expr)};"],
|
||||
lambda: new_lambda_pvariable(
|
||||
id_obj, lambda_expr, stateless_class, template_arg
|
||||
),
|
||||
)
|
||||
)
|
||||
):
|
||||
return _COLLECTED_ACTION
|
||||
@@ -1311,7 +1320,7 @@ async def condition_group_to_code(
|
||||
|
||||
operator = " || " if condition_id.type is OrCondition else " && "
|
||||
check = operator.join(_condition_expression(item, args) for item in items)
|
||||
_collect_action(condition_id, [f"return {check};"], emit)
|
||||
_collect_action(_InlineAction(condition_id, [f"return {check};"], emit))
|
||||
return _COLLECTED_ACTION
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return _new_condition_group(condition_id, template_arg, conditions)
|
||||
@@ -1338,11 +1347,13 @@ async def not_condition_to_code(
|
||||
if _is_collecting(condition_id):
|
||||
item = await _build_condition_or_inline(config, template_arg, args)
|
||||
_collect_action(
|
||||
condition_id,
|
||||
[f"return !{_condition_expression(item, args)};"],
|
||||
lambda: cg.new_Pvariable(
|
||||
condition_id, template_arg, _condition_object(item)
|
||||
),
|
||||
_InlineAction(
|
||||
condition_id,
|
||||
[f"return !{_condition_expression(item, args)};"],
|
||||
lambda: cg.new_Pvariable(
|
||||
condition_id, template_arg, _condition_object(item)
|
||||
),
|
||||
)
|
||||
)
|
||||
return _COLLECTED_ACTION
|
||||
condition = await build_condition(config, template_arg, args)
|
||||
@@ -1924,16 +1935,21 @@ async def _build_action_plan(
|
||||
"""Build the actions of ``config``; adjacent inline actions stay together, not yet emitted."""
|
||||
plan: list[MockObj | list[_InlineAction]] = []
|
||||
for conf in config:
|
||||
action = await _build_action_or_inline(conf, templ, arg_type)
|
||||
if not isinstance(action, _InlineAction):
|
||||
plan.append(action)
|
||||
elif plan and isinstance(plan[-1], list):
|
||||
plan[-1].append(action)
|
||||
else:
|
||||
plan.append([action])
|
||||
_plan_append(plan, await _build_action_or_inline(conf, templ, arg_type))
|
||||
return plan
|
||||
|
||||
|
||||
def _plan_append(
|
||||
plan: list[MockObj | list[_InlineAction]], action: MockObj | _InlineAction
|
||||
) -> None:
|
||||
if not isinstance(action, _InlineAction):
|
||||
plan.append(action)
|
||||
elif plan and isinstance(plan[-1], list):
|
||||
plan[-1].append(action)
|
||||
else:
|
||||
plan.append([action])
|
||||
|
||||
|
||||
def _emit_action_plan(
|
||||
plan: list[MockObj | list[_InlineAction]],
|
||||
templ: cg.TemplateArguments,
|
||||
@@ -2080,8 +2096,7 @@ async def new_automation(
|
||||
async def _build_automation_plan(
|
||||
args: TemplateArgsType, config: ConfigType
|
||||
) -> list[MockObj | list[_InlineAction]]:
|
||||
templ = cg.TemplateArguments(*(arg[0] for arg in args))
|
||||
return await _build_action_plan(config[CONF_THEN], templ, args)
|
||||
return await _build_action_plan(config[CONF_THEN], _args_templ(args), args)
|
||||
|
||||
|
||||
def _automation_from_plan(
|
||||
@@ -2090,7 +2105,7 @@ def _automation_from_plan(
|
||||
config: ConfigType,
|
||||
*ctor_args: MockObj,
|
||||
) -> MockObj:
|
||||
templ = cg.TemplateArguments(*(arg[0] for arg in args))
|
||||
templ = _args_templ(args)
|
||||
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, *ctor_args)
|
||||
for action in _emit_action_plan(plan, templ, args):
|
||||
cg.add(obj.add_action(action))
|
||||
@@ -2110,19 +2125,34 @@ def _is_direct(plan: list[MockObj | list[_InlineAction]], config: ConfigType) ->
|
||||
)
|
||||
|
||||
|
||||
def _direct_statements(
|
||||
plan: list[MockObj | list[_InlineAction]], config: ConfigType
|
||||
) -> list[str] | None:
|
||||
if not _is_direct(plan, config):
|
||||
return None
|
||||
return [line for line in _run_statements(plan[0]) if line != _STOP_CHECK]
|
||||
|
||||
|
||||
def _direct_function(
|
||||
plan: list[MockObj | list[_InlineAction]],
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
) -> MockObj | None:
|
||||
if not _is_direct(plan, config):
|
||||
if (statements := _direct_statements(plan, config)) is None:
|
||||
return None
|
||||
statements = [line for line in _run_statements(plan[0]) if line != _STOP_CHECK]
|
||||
return apply_static_function(
|
||||
plan[0][0].id_, cg.void, _apply_params(args), statements
|
||||
)
|
||||
|
||||
|
||||
def _args_templ(args: TemplateArgsType) -> cg.TemplateArguments:
|
||||
return cg.TemplateArguments(*(arg[0] for arg in args))
|
||||
|
||||
|
||||
def _arg_names(args: TemplateArgsType) -> str:
|
||||
return ", ".join(name for _, name in args)
|
||||
|
||||
|
||||
def _when_call(
|
||||
when: str | ApplyCall | None, parent: MockObj | None
|
||||
) -> ApplyCall | None:
|
||||
@@ -2140,6 +2170,33 @@ async def build_automation(
|
||||
return await new_automation(args, config, trigger)
|
||||
|
||||
|
||||
def declare_automation_function(
|
||||
args: TemplateArgsType, config: ConfigType, after: list[str] | None = None
|
||||
) -> tuple[MockObj, Callable[[], Awaitable[None]]]:
|
||||
"""Declare a ``static void`` function that runs ``config``; return its name and the coroutine that defines its body.
|
||||
|
||||
Await it once the object naming the function exists; ``after`` runs after the last action unless stopped.
|
||||
"""
|
||||
automation_id = config[CONF_AUTOMATION_ID]
|
||||
name = _apply_function_name(automation_id)
|
||||
define = cg.declare_static_function(name, cg.void, _apply_params(args))
|
||||
|
||||
async def build() -> None:
|
||||
plan = await _build_automation_plan(args, config)
|
||||
if after:
|
||||
after_id = cg.unique_global_id(f"{automation_id.id}__after", ApplyAction)
|
||||
_plan_append(
|
||||
plan,
|
||||
_inline_action(after_id, cg.void, _args_templ(args), args, after),
|
||||
)
|
||||
if (body := _direct_statements(plan, config)) is None:
|
||||
automation = _automation_from_plan(plan, args, config)
|
||||
body = [f"{parent_ref(automation)}->trigger({_arg_names(args)});"]
|
||||
define(body)
|
||||
|
||||
return cg.RawExpression(name), build
|
||||
|
||||
|
||||
async def build_trigger_callback(
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
|
||||
@@ -31,6 +31,7 @@ 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,
|
||||
|
||||
@@ -100,11 +100,7 @@ HomeAssistantActionResponseTrigger = api_ns.class_(
|
||||
"HomeAssistantActionResponseTrigger", automation.Trigger
|
||||
)
|
||||
APIRespondAction = api_ns.class_("APIRespondAction", automation.Action)
|
||||
APIUnregisterServiceCallAction = api_ns.class_(
|
||||
"APIUnregisterServiceCallAction", automation.Action
|
||||
)
|
||||
|
||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
||||
UserServiceCallback = api_ns.class_("UserServiceCallback")
|
||||
ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument")
|
||||
# Owning element type for each YAML service variable type. Used to derive both
|
||||
# the zero-copy native types and the owning fallback types below.
|
||||
@@ -313,9 +309,23 @@ VARIABLE_SCHEMA = cv.All(
|
||||
validate_variable = cv.maybe_simple_value(VARIABLE_SCHEMA, key=CONF_TYPE)
|
||||
|
||||
|
||||
def _validate_reserved_variables(config: ConfigType) -> ConfigType:
|
||||
"""Reject variables named like the arguments a response service passes first."""
|
||||
mode = config[CONF_SUPPORTS_RESPONSE]
|
||||
reserved = {"none": (), "optional": ("call_id", "return_response")}
|
||||
for name in reserved.get(mode, ("call_id",)):
|
||||
if name in config[CONF_VARIABLES]:
|
||||
raise cv.Invalid(
|
||||
f"'{name}' is reserved with supports_response: {mode}, "
|
||||
"give the variable another name",
|
||||
path=[CONF_VARIABLES, name],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
ACTIONS_SCHEMA = automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceCallback),
|
||||
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name,
|
||||
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name,
|
||||
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
|
||||
@@ -338,6 +348,7 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
{cv.Required(CONF_SUPPORTS_RESPONSE): _validate_supports_response},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
),
|
||||
_validate_reserved_variables,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -631,8 +642,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
has_optional = _has_optional_args(actions)
|
||||
if has_optional:
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS")
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.MockObj] = []
|
||||
# Collect all services first, then register all at once with initializer_list
|
||||
services: list[cg.MockObj] = []
|
||||
for index, conf in enumerate(actions):
|
||||
func_args: list[tuple[MockObj, str]] = []
|
||||
service_template_args: list[MockObj] = [] # User service argument types
|
||||
@@ -645,7 +656,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
is_optional = supports_response_key == "optional"
|
||||
|
||||
# Add call_id and return_response based on supports_response mode
|
||||
# These must match the C++ Trigger template arguments
|
||||
# These must match the arguments UserServiceCallback passes
|
||||
# - none: no extra args
|
||||
# - status: call_id only (for reporting success/error without data)
|
||||
# - only: call_id only (response always expected with data)
|
||||
@@ -676,32 +687,24 @@ async def to_code(config: ConfigType) -> None:
|
||||
func_args.append((native, name))
|
||||
strings = _action_strings(conf, has_metadata, has_optional)
|
||||
table = _add_action_strings(index, strings)
|
||||
# Template args: supports_response mode, then user service arg types
|
||||
templ = cg.TemplateArguments(supports_response, *service_template_args)
|
||||
# Key is hashed here because the name is not readable at runtime on ESP8266
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], templ, table, fnv1_hash(conf[CONF_ACTION])
|
||||
)
|
||||
triggers.append(trigger)
|
||||
auto = await automation.build_automation(trigger, func_args, conf)
|
||||
|
||||
# For non-none response modes, automatically append unregister action
|
||||
# This ensures the call is unregistered after all actions complete (including async ones)
|
||||
# A call that can respond is unregistered once every action has finished
|
||||
after = None
|
||||
if not is_none:
|
||||
arg_types = [arg[0] for arg in func_args]
|
||||
action_templ = cg.TemplateArguments(*arg_types)
|
||||
unregister_id = ID(
|
||||
f"{conf[CONF_TRIGGER_ID]}__unregister",
|
||||
is_declaration=True,
|
||||
type=APIUnregisterServiceCallAction.template(action_templ),
|
||||
server = automation.parent_ref(var)
|
||||
after = [
|
||||
f"if (call_id != 0)\n {server}->unregister_active_action_call(call_id);"
|
||||
]
|
||||
fn, build = automation.declare_automation_function(func_args, conf, after)
|
||||
templ = cg.TemplateArguments(fn, supports_response, *service_template_args)
|
||||
# Key is hashed here because the name is not readable at runtime on ESP8266
|
||||
services.append(
|
||||
cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], templ, table, fnv1_hash(conf[CONF_ACTION])
|
||||
)
|
||||
unregister_action = cg.new_Pvariable(
|
||||
unregister_id,
|
||||
var,
|
||||
)
|
||||
cg.add(auto.add_action(unregister_action))
|
||||
)
|
||||
await build()
|
||||
# Register all services at once - single allocation, no reallocations
|
||||
cg.add(var.initialize_user_services(triggers))
|
||||
cg.add(var.initialize_user_services(services))
|
||||
|
||||
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
|
||||
await wizard.to_code(wizard_config)
|
||||
|
||||
@@ -2297,8 +2297,8 @@ void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg)
|
||||
ESP_LOGV(TAG, "Could not find service");
|
||||
}
|
||||
// Note: For services with supports_response != none, the call is unregistered
|
||||
// by an automatically appended APIUnregisterServiceCallAction at the end of
|
||||
// the action list. This ensures async actions (delays, waits) complete first.
|
||||
// by statements generated after the last action, so async actions (delays,
|
||||
// waits) complete first.
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message) {
|
||||
|
||||
@@ -174,59 +174,25 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
uint32_t key_{0};
|
||||
};
|
||||
|
||||
// Primary template declaration
|
||||
template<enums::SupportsResponseType Mode, typename... Ts> class UserServiceTrigger;
|
||||
|
||||
// Specialization for NONE - no extra trigger arguments
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> final : public UserServiceBase<Ts...>,
|
||||
public Trigger<Ts...> {
|
||||
/// A YAML-defined action that calls the generated function Fn. Fn takes the call id first unless
|
||||
/// Mode is SUPPORTS_RESPONSE_NONE, then return_response for SUPPORTS_RESPONSE_OPTIONAL.
|
||||
template<auto Fn, enums::SupportsResponseType Mode, typename... Ts>
|
||||
class UserServiceCallback final : public UserServiceBase<Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *const *strings, uint32_t key)
|
||||
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_NONE) {}
|
||||
UserServiceCallback(const char *const *strings, uint32_t key) : UserServiceBase<Ts...>(strings, key, Mode) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
|
||||
};
|
||||
|
||||
// Specialization for OPTIONAL - call_id and return_response trigger arguments
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> final : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, bool, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *const *strings, uint32_t key)
|
||||
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool return_response, Ts... x) override {
|
||||
this->trigger(call_id, return_response, x...);
|
||||
void execute([[maybe_unused]] uint32_t call_id, [[maybe_unused]] bool return_response, Ts... x) override {
|
||||
if constexpr (Mode == enums::SUPPORTS_RESPONSE_NONE) {
|
||||
Fn(x...);
|
||||
} else if constexpr (Mode == enums::SUPPORTS_RESPONSE_OPTIONAL) {
|
||||
Fn(call_id, return_response, x...);
|
||||
} else {
|
||||
Fn(call_id, x...);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Specialization for ONLY - just call_id trigger argument
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> final : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *const *strings, uint32_t key)
|
||||
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_ONLY) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
|
||||
};
|
||||
|
||||
// Specialization for STATUS - just call_id trigger argument (reports success/error without data)
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> final : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *const *strings, uint32_t key)
|
||||
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_STATUS) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_USER_DEFINED_ACTIONS
|
||||
|
||||
@@ -301,24 +267,5 @@ template<typename... Ts> class APIRespondAction final : public Action<Ts...> {
|
||||
bool is_optional_mode_{false};
|
||||
};
|
||||
|
||||
// Action to unregister a service call after execution completes
|
||||
// Automatically appended to the end of action lists for non-none response modes
|
||||
template<typename... Ts> class APIUnregisterServiceCallAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// Extract call_id from first argument - same convention as APIRespondAction
|
||||
auto args = std::forward_as_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
if (call_id != 0) {
|
||||
this->parent_->unregister_active_action_call(call_id);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
APIServer *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
+23
-12
@@ -883,6 +883,28 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
|
||||
return Pvariable(id_, rhs)
|
||||
|
||||
|
||||
def declare_static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
) -> Callable[[list[str | Statement]], RawExpression]:
|
||||
"""Declare ``static <return_type> <name>(parameters)`` now; return the function that emits the body later.
|
||||
|
||||
The body goes after the user includes, where every id is in scope.
|
||||
"""
|
||||
signature = (
|
||||
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
|
||||
)
|
||||
add_global(RawStatement(f"{signature};"))
|
||||
|
||||
def define(body: list[str | Statement]) -> RawExpression:
|
||||
lines = indent("\n".join(map(str, body)))
|
||||
add_global_after_includes(RawStatement(f"{signature} {{\n{lines}\n}}"))
|
||||
return RawExpression(name)
|
||||
|
||||
return define
|
||||
|
||||
|
||||
def static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
@@ -891,19 +913,8 @@ def static_function(
|
||||
) -> 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
|
||||
headers declare is in scope.
|
||||
"""
|
||||
signature = (
|
||||
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
|
||||
)
|
||||
add_global(RawStatement(f"{signature};"))
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
return declare_static_function(name, return_type, parameters)(body)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for arg-type selection of api user-defined services with homeassistant.action."""
|
||||
|
||||
import re
|
||||
|
||||
CONFIG = "tests/component_tests/api/test_homeassistant_action.yaml"
|
||||
|
||||
|
||||
@@ -7,9 +9,10 @@ def test_synchronous_chain_keeps_zero_copy_args(generate_main):
|
||||
"""A chain of synchronous actions keeps the non-owning StringRef arg type."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
|
||||
assert (
|
||||
"api::UserServiceTrigger<api::enums::SUPPORTS_RESPONSE_NONE, StringRef>"
|
||||
"(api_action0_strings," in main_cpp
|
||||
assert re.search(
|
||||
r"api::UserServiceCallback<esphome__\w+__fn, "
|
||||
r"api::enums::SUPPORTS_RESPONSE_NONE, StringRef>\(api_action0_strings,",
|
||||
main_cpp,
|
||||
)
|
||||
|
||||
|
||||
@@ -20,9 +23,10 @@ def test_response_callback_args_are_owning(generate_main):
|
||||
receive buffer, which is reused before the response arrives."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
|
||||
assert (
|
||||
"api::UserServiceTrigger<api::enums::SUPPORTS_RESPONSE_NONE, std::string>"
|
||||
"(api_action1_strings," in main_cpp
|
||||
assert re.search(
|
||||
r"api::UserServiceCallback<esphome__\w+__fn, "
|
||||
r"api::enums::SUPPORTS_RESPONSE_NONE, std::string>\(api_action1_strings,",
|
||||
main_cpp,
|
||||
)
|
||||
assert "api::HomeAssistantServiceCallAction<std::string>" in main_cpp
|
||||
assert "api::HomeAssistantServiceCallAction<StringRef>" not in main_cpp
|
||||
@@ -7,6 +7,10 @@ auto-numbered ids, so adding or changing one action does not break the others.
|
||||
import re
|
||||
from typing import NamedTuple
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.api import ACTIONS_SCHEMA
|
||||
from esphome.config_validation import Invalid
|
||||
from esphome.helpers import fnv1_hash
|
||||
|
||||
CONFIG = "tests/component_tests/api/test_user_services.yaml"
|
||||
@@ -15,6 +19,10 @@ LEGACY_CONFIG = "tests/component_tests/api/test_user_services_legacy.yaml"
|
||||
NONE = "api::enums::SUPPORTS_RESPONSE_NONE"
|
||||
OPTIONAL = "api::enums::SUPPORTS_RESPONSE_OPTIONAL"
|
||||
ONLY = "api::enums::SUPPORTS_RESPONSE_ONLY"
|
||||
STATUS = "api::enums::SUPPORTS_RESPONSE_STATUS"
|
||||
UNREGISTER = (
|
||||
"if (call_id != 0)\n ::api_apiserver_id->unregister_active_action_call(call_id);"
|
||||
)
|
||||
|
||||
|
||||
class Service(NamedTuple):
|
||||
@@ -61,17 +69,31 @@ def _service(main_cpp: str, name: str) -> Service:
|
||||
return Service(match.group(1), match.group(2))
|
||||
|
||||
|
||||
def _automation(main_cpp: str, trigger: str) -> tuple[str, str]:
|
||||
"""Return the id and template arguments of the Automation on ``trigger``."""
|
||||
match = re.search(rf"\bnew\((\w+)\) Automation<(.*?)>\({trigger}\);", main_cpp)
|
||||
assert match is not None, trigger
|
||||
return match.group(1), match.group(2)
|
||||
def _service_function(main_cpp: str, name: str, mode: str, types: str = "") -> str:
|
||||
"""Check action ``name`` is a UserServiceCallback and return the function it calls."""
|
||||
service = _service(main_cpp, name)
|
||||
tail = f", {types}" if types else ""
|
||||
match = re.fullmatch(
|
||||
rf"api::UserServiceCallback<(\w+), {re.escape(mode + tail)}>", service.type
|
||||
)
|
||||
assert match is not None, service.type
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _triggered_automation(main_cpp: str, function: str, args: str) -> str:
|
||||
"""Check ``function`` only triggers an Automation with ``args``; return its id."""
|
||||
match = re.fullmatch(
|
||||
rf"\n ::(\w+)->trigger\({re.escape(args)}\);",
|
||||
_function_body(main_cpp, function),
|
||||
)
|
||||
assert match is not None, function
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _action_code(main_cpp: str, action: str) -> str:
|
||||
"""Return the code an action runs: its generated function, or its constructor."""
|
||||
constructed = _constructed(main_cpp, action)
|
||||
if match := re.match(r"ApplyRunAction<(\w+)[,>]", constructed):
|
||||
if match := re.match(r"Apply(?:Run)?Action<(\w+)[,>]", constructed):
|
||||
return _function_body(main_cpp, match.group(1))
|
||||
return constructed
|
||||
|
||||
@@ -80,18 +102,27 @@ def _action_class(main_cpp: str, action: str) -> str:
|
||||
return re.match(r"[\w:]+", _constructed(main_cpp, action)).group(0)
|
||||
|
||||
|
||||
def _assert_unregister_after(main_cpp: str, automation: str, types: str) -> None:
|
||||
"""The last action of ``automation`` only unregisters the call."""
|
||||
last = _actions_of(main_cpp, automation)[-1]
|
||||
assert last == f"{automation}__after"
|
||||
assert _constructed(main_cpp, last) == (
|
||||
f"ApplyAction<esphome__{automation}__after__fn, {types}>()"
|
||||
)
|
||||
assert _action_code(main_cpp, last) == f"\n {UNREGISTER}"
|
||||
|
||||
|
||||
def test_no_args_service(generate_main) -> None:
|
||||
"""A service without arguments triggers an Automation of its merged inline actions."""
|
||||
"""Inline actions run in one function the service calls, with no Automation."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
service = _service(main_cpp, "no_args")
|
||||
assert service.type == f"api::UserServiceTrigger<{NONE}>"
|
||||
automation, args = _automation(main_cpp, service.id)
|
||||
assert args == ""
|
||||
(action,) = _actions_of(main_cpp, automation)
|
||||
assert _action_class(main_cpp, action) == "ApplyRunAction"
|
||||
code = _action_code(main_cpp, action)
|
||||
assert "NO_ARGS called" in code
|
||||
assert "::doubled->value() = 1;" in code
|
||||
function = _service_function(main_cpp, "no_args", NONE)
|
||||
body = _function_body(main_cpp, function)
|
||||
assert "NO_ARGS called" in body
|
||||
assert "::doubled->value() = 1;" in body
|
||||
assert "esphome__running" not in body
|
||||
assert "Automation<>" not in main_cpp
|
||||
# The service calls the function directly, with no action object around it
|
||||
assert f"Action<{function}" not in main_cpp
|
||||
|
||||
|
||||
def test_mixed_args_types(generate_main) -> None:
|
||||
@@ -101,40 +132,38 @@ def test_mixed_args_types(generate_main) -> None:
|
||||
"bool, int32_t, float, StringRef, const FixedVector<int32_t> &, "
|
||||
"const FixedVector<std::string> &"
|
||||
)
|
||||
service = _service(main_cpp, "mixed_args")
|
||||
assert service.type == f"api::UserServiceTrigger<{NONE}, {types}>"
|
||||
automation, args = _automation(main_cpp, service.id)
|
||||
assert args == types
|
||||
function = _service_function(main_cpp, "mixed_args", NONE, types)
|
||||
assert _strings(main_cpp, "mixed_args")[1] == (
|
||||
'"mixed_args", "bool_", "int_", "float_", "string_", "int_arr", "string_arr"'
|
||||
)
|
||||
(action,) = _actions_of(main_cpp, automation)
|
||||
assert "string_.c_str(), int_arr.size(), string_arr.size()" in _action_code(
|
||||
main_cpp, action
|
||||
# Arrays stay non-owning references in the generated function
|
||||
assert re.search(
|
||||
rf"static void {function}\([^)]*"
|
||||
r"const FixedVector<int32_t> & int_arr, "
|
||||
r"const FixedVector<std::string> & string_arr\) \{",
|
||||
main_cpp,
|
||||
)
|
||||
body = _function_body(main_cpp, function)
|
||||
assert "string_.c_str(), int_arr.size(), string_arr.size()" in body
|
||||
|
||||
|
||||
def test_lambda_action_gets_args_by_value(generate_main) -> None:
|
||||
"""A lambda action takes the arguments by value, so ``x *= 2`` compiles in both forms."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
service = _service(main_cpp, "by_value")
|
||||
assert service.type == f"api::UserServiceTrigger<{NONE}, int32_t>"
|
||||
automation, _ = _automation(main_cpp, service.id)
|
||||
(action,) = _actions_of(main_cpp, automation)
|
||||
code = _action_code(main_cpp, action)
|
||||
assert code.count("[](int32_t x) -> void {") == 2
|
||||
assert "x *= 2;" in code
|
||||
assert '"BY_VALUE again %d", x *= 2' in code
|
||||
function = _service_function(main_cpp, "by_value", NONE, "int32_t")
|
||||
body = _function_body(main_cpp, function)
|
||||
assert body.count("[](int32_t x) -> void {") == 2
|
||||
assert "x *= 2;" in body
|
||||
assert '"BY_VALUE again %d", x *= 2' in body
|
||||
|
||||
|
||||
def test_delay_uses_owning_args(generate_main) -> None:
|
||||
"""A delay keeps separate actions and owning string and array arguments."""
|
||||
"""A delay keeps an Automation of separate actions, which the function triggers."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
types = "std::string, std::vector<int32_t>"
|
||||
service = _service(main_cpp, "with_delay")
|
||||
assert service.type == f"api::UserServiceTrigger<{NONE}, {types}>"
|
||||
automation, args = _automation(main_cpp, service.id)
|
||||
assert args == types
|
||||
function = _service_function(main_cpp, "with_delay", NONE, types)
|
||||
automation = _triggered_automation(main_cpp, function, "name, int_arr")
|
||||
assert _constructed(main_cpp, automation) == f"Automation<{types}>()"
|
||||
before, delay, after = _actions_of(main_cpp, automation)
|
||||
assert _action_class(main_cpp, delay) == "DelayAction"
|
||||
assert "WITH_DELAY before" in _action_code(main_cpp, before)
|
||||
@@ -146,49 +175,67 @@ def test_delay_uses_owning_args(generate_main) -> None:
|
||||
def test_respond_optional(generate_main) -> None:
|
||||
"""An optional response passes call_id and return_response; the call is unregistered last."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
service = _service(main_cpp, "respond_optional")
|
||||
assert service.type == f"api::UserServiceTrigger<{OPTIONAL}, int32_t>"
|
||||
automation, args = _automation(main_cpp, service.id)
|
||||
assert args == "uint32_t, bool, int32_t"
|
||||
respond, unregister = _actions_of(main_cpp, automation)
|
||||
types = "uint32_t, bool, int32_t"
|
||||
function = _service_function(main_cpp, "respond_optional", OPTIONAL, "int32_t")
|
||||
automation = _triggered_automation(
|
||||
main_cpp, function, "call_id, return_response, value"
|
||||
)
|
||||
assert _constructed(main_cpp, automation) == f"Automation<{types}>()"
|
||||
respond, _ = _actions_of(main_cpp, automation)
|
||||
assert _constructed(main_cpp, respond) == (
|
||||
"api::APIRespondAction<uint32_t, bool, int32_t>(api_apiserver_id)"
|
||||
f"api::APIRespondAction<{types}>(api_apiserver_id)"
|
||||
)
|
||||
assert f"{respond}->set_is_optional_mode(true);" in main_cpp
|
||||
assert unregister == f"{service.id}__unregister"
|
||||
assert _constructed(main_cpp, unregister) == (
|
||||
"api::APIUnregisterServiceCallAction<uint32_t, bool, int32_t>(api_apiserver_id)"
|
||||
)
|
||||
_assert_unregister_after(main_cpp, automation, types)
|
||||
|
||||
|
||||
def test_respond_only(generate_main) -> None:
|
||||
"""A response-only service passes call_id; the call is unregistered after every action."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
service = _service(main_cpp, "respond_only")
|
||||
assert service.type == f"api::UserServiceTrigger<{ONLY}, StringRef>"
|
||||
automation, args = _automation(main_cpp, service.id)
|
||||
assert args == "uint32_t, StringRef"
|
||||
log, respond, unregister = _actions_of(main_cpp, automation)
|
||||
types = "uint32_t, StringRef"
|
||||
function = _service_function(main_cpp, "respond_only", ONLY, "StringRef")
|
||||
automation = _triggered_automation(main_cpp, function, "call_id, name")
|
||||
assert _constructed(main_cpp, automation) == f"Automation<{types}>()"
|
||||
log, respond, _ = _actions_of(main_cpp, automation)
|
||||
assert "RESPOND_ONLY %s" in _action_code(main_cpp, log)
|
||||
assert _constructed(main_cpp, respond) == (
|
||||
"api::APIRespondAction<uint32_t, StringRef>(api_apiserver_id)"
|
||||
f"api::APIRespondAction<{types}>(api_apiserver_id)"
|
||||
)
|
||||
assert f"{respond}->set_is_optional_mode" not in main_cpp
|
||||
assert unregister == f"{service.id}__unregister"
|
||||
assert _constructed(main_cpp, unregister) == (
|
||||
"api::APIUnregisterServiceCallAction<uint32_t, StringRef>(api_apiserver_id)"
|
||||
)
|
||||
_assert_unregister_after(main_cpp, automation, types)
|
||||
|
||||
|
||||
def test_unregister_merges_with_trailing_inline_actions(generate_main) -> None:
|
||||
"""After a non-inline action, the unregister joins the inline actions that follow it."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
function = _service_function(main_cpp, "respond_then_log", STATUS)
|
||||
automation = _triggered_automation(main_cpp, function, "call_id")
|
||||
respond, log = _actions_of(main_cpp, automation)
|
||||
assert _action_class(main_cpp, respond) == "api::APIRespondAction"
|
||||
body = _action_code(main_cpp, log)
|
||||
assert body.index("RESPONDED") < body.index("if (!esphome__running)")
|
||||
assert body.endswith(f"{UNREGISTER}\n }}")
|
||||
|
||||
|
||||
def test_unregister_in_fully_inline_response_service(generate_main) -> None:
|
||||
"""With only inline actions, the unregister ends the function the service calls."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
function = _service_function(main_cpp, "status_inline", STATUS)
|
||||
body = _function_body(main_cpp, function)
|
||||
assert "->trigger(" not in body
|
||||
assert "esphome__running" not in body
|
||||
assert f"Action<{function}" not in main_cpp
|
||||
assert body.index("STATUS_INLINE first") < body.index("send_action_response")
|
||||
assert body.endswith(f"\n {{\n {UNREGISTER}\n }}")
|
||||
|
||||
|
||||
def test_manual_trigger_id(generate_main) -> None:
|
||||
"""A trigger_id set by hand names the service object."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _service(main_cpp, "manual_trigger_id") == Service(
|
||||
"manual_service_trigger", f"api::UserServiceTrigger<{NONE}>"
|
||||
)
|
||||
automation, _ = _automation(main_cpp, "manual_service_trigger")
|
||||
(action,) = _actions_of(main_cpp, automation)
|
||||
assert "MANUAL called" in _action_code(main_cpp, action)
|
||||
service = _service(main_cpp, "manual_trigger_id")
|
||||
assert service.id == "manual_service_trigger"
|
||||
function = _service_function(main_cpp, "manual_trigger_id", NONE)
|
||||
assert "MANUAL called" in _function_body(main_cpp, function)
|
||||
|
||||
|
||||
def test_lambda_uses_own_service_id(generate_main) -> None:
|
||||
@@ -196,9 +243,34 @@ def test_lambda_uses_own_service_id(generate_main) -> None:
|
||||
main_cpp = generate_main(CONFIG)
|
||||
service = _service(main_cpp, "self_reference")
|
||||
assert service.id == "self_reference_service"
|
||||
automation, _ = _automation(main_cpp, service.id)
|
||||
(action,) = _actions_of(main_cpp, automation)
|
||||
assert "(const void *) self_reference_service" in _action_code(main_cpp, action)
|
||||
function = _service_function(main_cpp, "self_reference", NONE)
|
||||
body = _function_body(main_cpp, function)
|
||||
assert "(const void *) self_reference_service" in body
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "name"),
|
||||
[
|
||||
("status", "call_id"),
|
||||
("optional", "call_id"),
|
||||
("optional", "return_response"),
|
||||
],
|
||||
)
|
||||
def test_reserved_variable_name_rejected(mode: str, name: str) -> None:
|
||||
"""A variable cannot take the name of an argument the response mode passes first."""
|
||||
action = {"action": "x", "supports_response": mode, "variables": {name: "int"}}
|
||||
with pytest.raises(Invalid, match=f"'{name}' is reserved"):
|
||||
ACTIONS_SCHEMA({**action, "then": []})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "name"), [("none", "call_id"), ("status", "return_response")]
|
||||
)
|
||||
def test_variable_name_free_in_other_modes(mode: str, name: str) -> None:
|
||||
"""The names are only reserved in the modes that pass those arguments."""
|
||||
action = {"action": "x", "supports_response": mode, "variables": {name: "int"}}
|
||||
(config,) = ACTIONS_SCHEMA({**action, "then": []})
|
||||
assert name in config["variables"]
|
||||
|
||||
|
||||
def test_services_registered_in_order(generate_main) -> None:
|
||||
@@ -212,7 +284,10 @@ def test_services_registered_in_order(generate_main) -> None:
|
||||
"respond_optional",
|
||||
"respond_only",
|
||||
"manual_trigger_id",
|
||||
"respond_then_log",
|
||||
"status_inline",
|
||||
"self_reference",
|
||||
"respond_named",
|
||||
]
|
||||
match = re.search(
|
||||
r"\bapi_apiserver_id->initialize_user_services\(\{(.*?)\}\);", main_cpp
|
||||
@@ -225,9 +300,15 @@ def test_legacy_services_key(generate_main) -> None:
|
||||
"""The legacy ``services:`` key with ``service:`` names builds the same service."""
|
||||
main_cpp = generate_main(LEGACY_CONFIG)
|
||||
assert _strings(main_cpp, "legacy_service")[1] == '"legacy_service", "devices"'
|
||||
service = _service(main_cpp, "legacy_service")
|
||||
assert service.type == f"api::UserServiceTrigger<{NONE}, StringRef>"
|
||||
automation, args = _automation(main_cpp, service.id)
|
||||
assert args == "StringRef"
|
||||
(action,) = _actions_of(main_cpp, automation)
|
||||
assert '"LEGACY %s", devices.c_str()' in _action_code(main_cpp, action)
|
||||
function = _service_function(main_cpp, "legacy_service", NONE, "StringRef")
|
||||
assert "Automation<StringRef>" not in main_cpp
|
||||
assert '"LEGACY %s", devices.c_str()' in _function_body(main_cpp, function)
|
||||
|
||||
|
||||
def test_cleanup_action_id_avoids_config_ids(generate_main) -> None:
|
||||
"""The response cleanup action does not take an id the config already uses."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
last = _actions_of(main_cpp, "worker")[-1]
|
||||
assert last.startswith("worker__after")
|
||||
assert last != "worker__after"
|
||||
assert _action_code(main_cpp, last) == f"\n {UNREGISTER}"
|
||||
@@ -14,6 +14,10 @@ globals:
|
||||
- id: doubled
|
||||
type: int
|
||||
initial_value: "0"
|
||||
# Same id the respond_named cleanup action would get without a uniqueness check
|
||||
- id: worker__after
|
||||
type: int
|
||||
initial_value: "0"
|
||||
|
||||
api:
|
||||
actions:
|
||||
@@ -83,9 +87,29 @@ api:
|
||||
trigger_id: manual_service_trigger
|
||||
then:
|
||||
- logger.log: "MANUAL called"
|
||||
- action: respond_then_log
|
||||
then:
|
||||
- api.respond:
|
||||
success: true
|
||||
- logger.log: "RESPONDED"
|
||||
# Only inline actions: the lambda reports the status itself
|
||||
- action: status_inline
|
||||
supports_response: status
|
||||
then:
|
||||
- logger.log: "STATUS_INLINE first"
|
||||
- lambda: |-
|
||||
api::global_api_server->send_action_response(call_id, true, StringRef());
|
||||
# A lambda may use the service's own id
|
||||
- action: self_reference
|
||||
trigger_id: self_reference_service
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("test", "SELF_REFERENCE %p", (const void *) id(self_reference_service));
|
||||
# A config id that the response cleanup action's generated id would otherwise take
|
||||
- action: respond_named
|
||||
automation_id: worker
|
||||
supports_response: only
|
||||
then:
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["ok"] = true;
|
||||
@@ -10,6 +10,7 @@ logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
id: api_server
|
||||
actions:
|
||||
# Action that responds immediately - should work fine
|
||||
- action: action_immediate
|
||||
@@ -42,3 +43,14 @@ api:
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["status"] = "long_delay";
|
||||
|
||||
# Only inline actions; the lambda sends the status itself. The call must be
|
||||
# unregistered after it, so it never times out.
|
||||
- action: action_inline_status
|
||||
supports_response: status
|
||||
then:
|
||||
- logger.log:
|
||||
format: "ACTION_INLINE_STATUS call_id=%u"
|
||||
args: [(unsigned) call_id]
|
||||
- lambda: |-
|
||||
id(api_server)->send_action_response(call_id, true, StringRef());
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -29,6 +30,7 @@ async def test_api_action_timeout(
|
||||
1. Actions that respond within the timeout work correctly
|
||||
2. Actions that exceed the timeout have their calls cleaned up
|
||||
3. Late responses log a warning but don't crash
|
||||
4. A call to a service of only inline actions is unregistered after them
|
||||
"""
|
||||
waiter = LineWaiter()
|
||||
|
||||
@@ -43,13 +45,26 @@ async def test_api_action_timeout(
|
||||
# List services
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Should have 3 services
|
||||
assert len(services) == 3, f"Expected 3 services, found {len(services)}"
|
||||
# Should have 4 services
|
||||
assert len(services) == 4, f"Expected 4 services, found {len(services)}"
|
||||
|
||||
# Find our services
|
||||
action_immediate = require_service(services, "action_immediate")
|
||||
action_short_delay = require_service(services, "action_short_delay")
|
||||
action_long_delay = require_service(services, "action_long_delay")
|
||||
action_inline_status = require_service(services, "action_inline_status")
|
||||
|
||||
# Test 0: an inline-only service responds from a lambda, then is unregistered;
|
||||
# the timed-out check at the end shows the unregister ran
|
||||
response = await client.execute_service(
|
||||
action_inline_status,
|
||||
{},
|
||||
return_response=True,
|
||||
)
|
||||
assert response is not None, "Expected response for inline status action"
|
||||
assert response.success is True
|
||||
line = await waiter.wait_for("ACTION_INLINE_STATUS call_id=", timeout=1.0)
|
||||
inline_call_id = re.search(r"call_id=(\d+)", line).group(1)
|
||||
|
||||
# Test 1: Immediate response should work
|
||||
response = await client.execute_service(
|
||||
@@ -114,3 +129,7 @@ async def test_api_action_timeout(
|
||||
)
|
||||
assert response is not None, "System should still work after timeout"
|
||||
assert response.success is True
|
||||
|
||||
# Well over 500 ms have passed: only the long delay call timed out
|
||||
assert waiter.count(re.compile(r"Action call \d+ timed out")) == 1
|
||||
assert waiter.count(f"Action call {inline_call_id} timed out") == 0
|
||||
@@ -1173,6 +1173,21 @@ 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