Compare commits

...
9 changed files with 200 additions and 164 deletions

No files matched your search

+4 -3
View File
@@ -646,7 +646,8 @@ def parent_ref(var: MockObj) -> MockObj:
return MockObj(f"::{var}", "->")
async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
async def apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
"""The object ``config[id_key]`` names, as ``parent_ref`` spells it."""
return str(parent_ref(await cg.get_variable(config[id_key])))
@@ -868,7 +869,7 @@ def apply_action_builder(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await _apply_parent(config, id_key)
parent = await apply_parent(config, id_key)
receiver = "apply_call." if call else f"{parent}->"
statements: list[str] = []
for spec in statements_spec:
@@ -1014,7 +1015,7 @@ def register_apply_condition(
) -> MockObj:
if defaults:
config = {**defaults, **config}
parent = await _apply_parent(config, id_key)
parent = await apply_parent(config, id_key)
values = _apply_values(config, members)
check = await _render_call(
name, call.target, members, values, config, parent, args, compare=True
+62 -37
View File
@@ -834,22 +834,16 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
)
async def _new_service_call_action(
server_id: ID,
action_id: ID,
async def _service_call_table(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
is_event: bool,
service: Any,
data: dict[str, Any],
data_template: dict[str, Any],
variables: dict[str, Any],
) -> MockObj:
"""Create the action with its name and fields in one shared flash table."""
groups: tuple[dict[str, Any], ...],
) -> list[Expression | int]:
"""Return the name and the data, data_template and variables ``groups`` in one shared
flash table, then the size of each map."""
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(server_id)
field_type = HomeAssistantField.template(template_arg)
groups = [data, data_template, variables]
# A lambda may keep static state, so a table with lambdas is never shared
has_lambda = cg.is_template(service) or any(
cg.is_template(value) for group in groups for value in group.values()
@@ -873,8 +867,30 @@ async def _new_service_call_action(
cg.ArrayInitializer(*entries, multiline=True),
share=not has_lambda,
)
return cg.new_Pvariable(
action_id, template_arg, serv, is_event, table, *(len(g) for g in groups)
return [table, *(len(g) for g in groups)]
async def _send_service_call(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
is_event: bool,
service: Any,
groups: tuple[dict[str, Any], ...],
) -> MockObj:
"""Send the action or event from a generated function, without waiting for a response."""
server = MockObj(await automation.apply_parent(config))
table, *counts = await _service_call_table(template_arg, args, service, groups)
call = api_ns.send_homeassistant_fields(
server,
is_event,
automation.parent_ref(table),
*counts,
*(MockObj(name) for _, name in args),
)
return automation.apply_statements_action(
action_id, template_arg, args, [f"{call};"]
)
@@ -887,13 +903,13 @@ def _service_call_fields(config: ConfigType) -> tuple[dict[str, Any], ...]:
# (StringRef into the API receive buffer) must not be used.
@automation.register_action(
"homeassistant.action",
HomeAssistantServiceCallAction,
automation.ApplyAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=False,
)
@automation.register_action(
"homeassistant.service",
HomeAssistantServiceCallAction,
automation.ApplyAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=False,
)
@@ -903,29 +919,40 @@ async def homeassistant_service_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = await _new_service_call_action(
config[CONF_ID],
on_error = config.get(CONF_ON_ERROR)
on_success = config.get(CONF_ON_SUCCESS)
if not on_error and not on_success:
return await _send_service_call(
config,
action_id,
template_arg,
args,
False,
config[CONF_ACTION],
_service_call_fields(config),
)
# Waiting for the response needs an object that holds the triggers
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
action_id.type = HomeAssistantServiceCallAction
var = cg.new_Pvariable(
action_id,
template_arg,
args,
False,
config[CONF_ACTION],
*_service_call_fields(config),
await cg.get_variable(config[CONF_ID]),
*await _service_call_table(
template_arg, args, config[CONF_ACTION], _service_call_fields(config)
),
)
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
if on_error:
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
cg.add(var.set_wants_status())
await automation.build_automation(
var.get_error_trigger(),
[(cg.std_string, "error"), *args],
on_error,
)
if on_success := config.get(CONF_ON_SUCCESS):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add(var.set_wants_status())
if on_success:
if config[CONF_CAPTURE_RESPONSE]:
cg.add(var.set_wants_response())
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
@@ -974,7 +1001,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
# so play() never stores the trigger args.
@automation.register_action(
"homeassistant.event",
HomeAssistantServiceCallAction,
automation.ApplyAction,
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
synchronous=True,
)
@@ -984,14 +1011,14 @@ async def homeassistant_event_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
return await _new_service_call_action(
config[CONF_ID],
return await _send_service_call(
config,
action_id,
template_arg,
args,
True,
config[CONF_EVENT],
*_service_call_fields(config),
_service_call_fields(config),
)
@@ -1006,7 +1033,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action(
"homeassistant.tag_scanned",
HomeAssistantServiceCallAction,
automation.ApplyAction,
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
synchronous=True,
)
@@ -1016,16 +1043,14 @@ async def homeassistant_tag_scanned_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
return await _new_service_call_action(
config[CONF_ID],
return await _send_service_call(
config,
action_id,
template_arg,
args,
True,
"esphome.tag_scanned",
{"tag_id": config[CONF_TAG]},
{},
{},
({"tag_id": config[CONF_TAG]}, {}, {}),
)
+77 -66
View File
@@ -81,86 +81,102 @@ class ActionResponse {
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
#endif
/// Called with the request just before it is sent, and the context passed with it.
using HomeAssistantPrepareFn = void (*)(HomeassistantActionRequest &, void *);
/// Builds the request from `fields` and the trigger args, calls `prepare` (when set), then sends it.
/// `fields` is a codegen table: the action or event name (no key), then the data, data_template
/// and variables entries.
template<typename... Ts>
void send_homeassistant_fields_with(HomeAssistantPrepareFn prepare, void *context, APIServer *server, bool is_event,
const HomeAssistantField<Ts...> *fields, uint8_t data_count,
uint8_t data_template_count, uint8_t variables_count,
const std::type_identity_t<Ts> &...x) {
using Field = HomeAssistantField<Ts...>;
const size_t total = 1 + data_count + data_template_count + variables_count;
// Lambda results must live until the send; the constant strings are encoded from flash where they are
size_t lambda_count = 0;
for (size_t i = 0; i < total; i++)
lambda_count += fields[i].fn != nullptr;
FixedVector<std::string> results;
results.init(lambda_count);
auto string_ref = [](const char *str) { return ProgmemStringRef(str, ESPHOME_strlen_P(str)); };
auto value_ref = [&](const Field &field) {
if (field.fn == nullptr)
return string_ref(field.value);
results.push_back(field.fn(x...));
return ProgmemStringRef(StringRef(results.back()));
};
auto fill = [&](FixedVector<HomeassistantServiceMap> &dest, uint8_t count) {
dest.init(count);
for (uint8_t i = 0; i < count; i++, fields++) {
auto &kv = dest.emplace_back();
kv.key = string_ref(fields->key);
kv.value = value_ref(*fields);
}
};
HomeassistantActionRequest resp;
resp.service = value_ref(*fields++);
resp.is_event = is_event;
fill(resp.data, data_count);
fill(resp.data_template, data_template_count);
fill(resp.variables, variables_count);
if (prepare != nullptr)
prepare(resp, context);
server->send_homeassistant_action(resp);
}
/// Sends an action or event that does not wait for a response; generated actions call it.
template<typename... Ts>
void send_homeassistant_fields(APIServer *server, bool is_event, const HomeAssistantField<Ts...> *fields,
uint8_t data_count, uint8_t data_template_count, uint8_t variables_count,
const std::type_identity_t<Ts> &...x) {
send_homeassistant_fields_with(nullptr, nullptr, server, is_event, fields, data_count, data_template_count,
variables_count, x...);
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
/// An action whose on_success or on_error automation runs when Home Assistant responds.
template<typename... Ts> class HomeAssistantServiceCallAction final : public Action<Ts...> {
public:
using Field = HomeAssistantField<Ts...>;
/// `fields` is a codegen table: the action or event name (no key), then the data, data_template
/// and variables entries.
HomeAssistantServiceCallAction(APIServer *parent, bool is_event, const Field *fields, uint8_t data_count,
/// `fields` is a table as for `send_homeassistant_fields_with`.
HomeAssistantServiceCallAction(APIServer *parent, const Field *fields, uint8_t data_count,
uint8_t data_template_count, uint8_t variables_count)
: parent_(parent),
fields_(fields),
data_count_(data_count),
data_template_count_(data_template_count),
variables_count_(variables_count) {
this->flags_.is_event = is_event;
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
this->flags_.has_response_template = true;
}
void set_wants_status() { this->flags_.wants_status = true; }
void set_wants_response() { this->flags_.wants_response = true; }
variables_count_(variables_count) {}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
template<typename T> void set_response_template(T response_template) { this->response_template_ = response_template; }
void set_wants_response() { this->wants_response_ = true; }
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() { return &this->success_trigger_with_response_; }
#endif
Trigger<Ts...> *get_success_trigger() { return &this->success_trigger_; }
Trigger<std::string, Ts...> *get_error_trigger() { return &this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(const Ts &...x) override {
const Field *fields = this->fields_;
const size_t total = 1 + this->data_count_ + this->data_template_count_ + this->variables_count_;
// Lambda results must live until the send; the constant strings are encoded from flash where they are
size_t lambda_count = 0;
for (size_t i = 0; i < total; i++)
lambda_count += fields[i].fn != nullptr;
FixedVector<std::string> results;
results.init(lambda_count);
auto string_ref = [](const char *str) { return ProgmemStringRef(str, ESPHOME_strlen_P(str)); };
auto value_ref = [&](const Field &field) {
if (field.fn == nullptr)
return string_ref(field.value);
results.push_back(field.fn(x...));
return ProgmemStringRef(StringRef(results.back()));
};
auto fill = [&](FixedVector<HomeassistantServiceMap> &dest, uint8_t count) {
dest.init(count);
for (uint8_t i = 0; i < count; i++, fields++) {
auto &kv = dest.emplace_back();
kv.key = string_ref(fields->key);
kv.value = value_ref(*fields);
}
};
HomeassistantActionRequest resp;
resp.service = value_ref(*fields++);
resp.is_event = this->flags_.is_event;
fill(resp.data, this->data_count_);
fill(resp.data_template, this->data_template_count_);
fill(resp.variables, this->variables_count_);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// IMPORTANT: Declare at outer scope so it lives until send_homeassistant_action returns.
std::string response_template_value;
#endif
if (this->flags_.wants_status) {
auto prepare = [&](HomeassistantActionRequest &resp) {
// Generate a unique call ID for this service call
static uint32_t call_id_counter = 1;
uint32_t call_id = call_id_counter++;
resp.call_id = call_id;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (this->flags_.wants_response) {
if (this->wants_response_) {
resp.wants_response = true;
// Set response template if provided
if (this->flags_.has_response_template) {
if (this->response_template_.has_value()) {
response_template_value = this->response_template_.value(x...);
resp.response_template = StringRef(response_template_value);
}
@@ -173,7 +189,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction final : public Act
[this, &response](auto &&...args) {
if (response.is_success()) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (this->flags_.wants_response) {
if (this->wants_response_) {
this->success_trigger_with_response_.trigger(response.get_json(), args...);
} else
#endif
@@ -186,10 +202,12 @@ template<typename... Ts> class HomeAssistantServiceCallAction final : public Act
},
captured_args);
});
}
#endif
this->parent_->send_homeassistant_action(resp);
};
// One body per Ts, shared with the sends that do not wait for a response
send_homeassistant_fields_with(
[](HomeassistantActionRequest &resp, void *context) { (*static_cast<decltype(prepare) *>(context))(resp); },
&prepare, this->parent_, false, this->fields_, this->data_count_, this->data_template_count_,
this->variables_count_, x...);
}
protected:
@@ -198,22 +216,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction final : public Act
uint8_t data_count_;
uint8_t data_template_count_;
uint8_t variables_count_;
struct Flags {
uint8_t is_event : 1;
uint8_t wants_status : 1;
uint8_t wants_response : 1;
uint8_t has_response_template : 1;
uint8_t reserved : 4;
} flags_{0};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
bool wants_response_{false};
TemplatableValue<std::string, Ts...> response_template_{};
Trigger<JsonObjectConst, Ts...> success_trigger_with_response_;
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<Ts...> success_trigger_;
Trigger<std::string, Ts...> error_trigger_;
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
};
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
} // namespace esphome::api
+2 -3
View File
@@ -1,5 +1,4 @@
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.automation import apply_parent
from esphome.components import button, output
import esphome.config_validation as cv
from esphome.const import CONF_DURATION, CONF_OUTPUT
@@ -14,7 +13,7 @@ CONFIG_SCHEMA = button.button_schema(button.ApplyButton).extend(
async def to_code(config: ConfigType) -> None:
out = parent_ref(await cg.get_variable(config[CONF_OUTPUT]))
out = await apply_parent(config, CONF_OUTPUT)
duration = config[CONF_DURATION].total_milliseconds
await button.sub_buttons(config).create(
config,
+2 -2
View File
@@ -253,7 +253,7 @@ async def script_execute_action_to_code(config, action_id, template_arg, args):
_, script_params = parameters_to_template(params)
script_args = await get_ordered_args(config, script_params)
paren = await cg.get_variable(config[CONF_ID])
parent = await automation.apply_parent(config)
# Separate statements, because the order of evaluation inside one call is not defined
statements = [
f"auto esphome__arg_{index} = {automation.immediate_lambda_call(arg)};"
@@ -262,7 +262,7 @@ async def script_execute_action_to_code(config, action_id, template_arg, args):
names = ", ".join(
f"std::move(esphome__arg_{index})" for index in range(len(script_args))
)
statements.append(f"{automation.parent_ref(paren)}->execute({names});")
statements.append(f"{parent}->execute({names});")
return automation.apply_statements_action(action_id, template_arg, args, statements)
@@ -19,10 +19,12 @@ def test_field_tables(generate_main: Callable[[str | Path], str]) -> None:
assert main_cpp.count('PROGMEM = "notify.notify";') == 1
assert 'ESPHOME_F("message")' not in main_cpp
assert main_cpp.count("(api_apiserver_id, false, ha_action_fields, 1, 0, 0);") == 2
assert (
main_cpp.count("(::api_apiserver_id, false, ::ha_action_fields, 1, 0, 0);") == 2
)
assert "(api_apiserver_id, true, ha_action_fields_2, 1, 0, 0);" in main_cpp
assert "(api_apiserver_id, true, ha_action_fields_3, 1, 0, 0);" in main_cpp
assert "(::api_apiserver_id, true, ::ha_action_fields_2, 1, 0, 0);" in main_cpp
assert "(::api_apiserver_id, true, ::ha_action_fields_3, 1, 0, 0);" in main_cpp
@pytest.mark.parametrize("schema", [DATA_FIELDS_SCHEMA, VARIABLES_FIELDS_SCHEMA])
@@ -6,6 +6,8 @@ import re
import pytest
from tests.component_tests.helpers import static_function_body
CONFIG = "tests/component_tests/api/test_homeassistant_send.yaml"
@@ -38,24 +40,18 @@ def test_field_order(main_cpp: str) -> None:
assert 'progmem_str_6[] PROGMEM = "name";' in main_cpp
SEND = "api::send_homeassistant_fields(::api_apiserver_id, "
def test_action_without_response(main_cpp: str) -> None:
"""An action with no response handling is sent with the counts of each map."""
assert (
"api::HomeAssistantServiceCallAction<>"
"(api_apiserver_id, false, ha_action_fields, 1, 1, 1);"
) in main_cpp
assert f"{SEND}false, ::ha_action_fields, 1, 1, 1);" in main_cpp
def test_event_and_tag_scanned(main_cpp: str) -> None:
"""Events and scanned tags are sent as events."""
assert (
"api::HomeAssistantServiceCallAction<>"
"(api_apiserver_id, true, ha_action_fields_5, 0, 0, 0);"
) in main_cpp
assert (
"api::HomeAssistantServiceCallAction<>"
"(api_apiserver_id, true, ha_action_fields_6, 1, 0, 0);"
) in main_cpp
assert f"{SEND}true, ::ha_action_fields_5, 0, 0, 0);" in main_cpp
assert f"{SEND}true, ::ha_action_fields_6, 1, 0, 0);" in main_cpp
assert " {progmem_str_13, progmem_str_14, nullptr}," in _table(
main_cpp, "ha_action_fields_6"
)
@@ -78,48 +74,47 @@ def test_lambdas_take_args_by_value(main_cpp: str, table: str, body: str) -> Non
def test_script_actions(main_cpp: str) -> None:
"""Script actions take the script parameters."""
assert (
"api::HomeAssistantServiceCallAction<int>"
"(api_apiserver_id, false, ha_action_fields_2, 2, 0, 0);"
) in main_cpp
assert (
"api::HomeAssistantServiceCallAction<int>"
"(api_apiserver_id, true, ha_action_fields_3, 1, 0, 0);"
) in main_cpp
"""Script actions pass the script parameters to the field lambdas."""
assert f"{SEND}false, ::ha_action_fields_2, 2, 0, 0, level);" in main_cpp
assert f"{SEND}true, ::ha_action_fields_3, 1, 0, 0, level);" in main_cpp
def test_action_with_response_is_an_object(main_cpp: str) -> None:
"""on_error waits for the response, so the action keeps its object and trigger."""
match = re.search(
r"new\((\w+)\) api::HomeAssistantServiceCallAction<>"
r"\(api_apiserver_id, false, ha_action_fields_4, 0, 0, 0\);",
r"\(api_apiserver_id, ha_action_fields_4, 0, 0, 0\);",
main_cpp,
)
assert match is not None
action = match.group(1)
assert f"{action}->set_wants_status();" in main_cpp
assert f"Automation<std::string>({action}->get_error_trigger());" in main_cpp
def test_action_then_log(main_cpp: str) -> None:
"""The action sends before the action after it runs."""
actions = re.findall(r"\bautomation_id->add_action\((\w+)\);", main_cpp)
assert actions == ["api_homeassistantservicecallaction_id", "lambdaaction_id"]
"""The send is an if-free statement, so the action merged after it still runs."""
assert re.findall(r"\bautomation_id->add_action\((\w+)\);", main_cpp) == [
"applyaction_id"
]
assert "ApplyRunAction<esphome__applyaction_id__fn>();" in main_cpp
body = static_function_body(main_cpp, "esphome__applyaction_id__fn")
assert body.index(f"{SEND}false, ::ha_action_fields,") < body.index(
'"after action"'
)
# The only return is the stop check between the two actions
assert body.count("return;") == 1
def test_button_on_press(main_cpp: str) -> None:
"""A button sends its event and scanned tag from its press callback."""
assert re.search(
r"add_on_press_callback\(TriggerForwarder<>\{automation_id_5\}\);", main_cpp
"""A press callback that only sends calls one generated function, with no objects."""
match = re.search(
r"add_on_press_callback\(\[\]\(\) -> void \{\n (\w+)\(\);\n\}\);",
main_cpp,
)
sends = {
match.group(2): match.group(1)
for match in re.finditer(
r"new\((\w+)\) api::HomeAssistantServiceCallAction<>"
r"\(api_apiserver_id, true, (ha_action_fields_[56]), [01], 0, 0\);",
main_cpp,
)
}
actions = re.findall(r"\bautomation_id_5->add_action\((\w+)\);", main_cpp)
assert actions == [sends["ha_action_fields_5"], sends["ha_action_fields_6"]]
assert match is not None
body = static_function_body(main_cpp, match.group(1))
assert body.index(f"{SEND}true, ::ha_action_fields_5, 0, 0, 0);") < body.index(
f"{SEND}true, ::ha_action_fields_6, 1, 0, 0);"
)
assert "TriggerForwarder" not in main_cpp
assert "Automation<>()" not in main_cpp
+11
View File
@@ -31,6 +31,17 @@ def extract_packed_value(main_cpp: str, var_name: str) -> int:
return int(match.group(1))
def static_function_body(main_cpp: str, name: str) -> str:
"""Body of the generated ``static void name(...)`` function, without its braces."""
match = re.search(
rf"static void {re.escape(name)}\([^;{{]*\) \{{\n(.*?)\n\}}",
main_cpp,
re.DOTALL,
)
assert match is not None, f"{name} not defined"
return match.group(1)
def get_define_value(name: str) -> str | None:
"""Rendered value of a CORE define, or None when absent.
@@ -12,19 +12,11 @@ import re
import pytest
from tests.component_tests.helpers import static_function_body
YAML = Path(__file__).parent / "button_dispatch.yaml"
def _fn_body(main_cpp: str, fn: str) -> str:
match = re.search(
rf"static void {fn}\(button::Button \* entity\) \{{\n(.*?)\n\}}",
main_cpp,
re.DOTALL,
)
assert match is not None, f"{fn} not defined"
return match.group(1)
@pytest.mark.parametrize(
("fn", "type_expr", "hub"),
[
@@ -58,7 +50,7 @@ def test_dispatch_function(
main_cpp = generate_main(YAML)
assert main_cpp.count(f"static void {fn}(button::Button * entity) {{") == 1
assert _fn_body(main_cpp, fn) == (
assert static_function_body(main_cpp, fn) == (
f" const auto type = {type_expr};\n ::{hub}->press_button(type);"
)