mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 14:22:58 +08:00
[api] Device defined action responses (#12136)
CI for docker images / Build docker containers (docker, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04-arm) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04-arm) (push) Has been cancelled
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
Stale / stale (push) Has been cancelled
Lock closed issues and PRs / lock (push) Has been cancelled
Publish Release / Initialize build (push) Has been cancelled
Publish Release / Build and publish to PyPi (push) Has been cancelled
Publish Release / Build ESPHome amd64 (push) Has been cancelled
Publish Release / Build ESPHome arm64 (push) Has been cancelled
Publish Release / Publish ESPHome docker to dockerhub (push) Has been cancelled
Publish Release / Publish ESPHome docker to ghcr (push) Has been cancelled
Publish Release / Publish ESPHome ha-addon to dockerhub (push) Has been cancelled
Publish Release / Publish ESPHome ha-addon to ghcr (push) Has been cancelled
Publish Release / deploy-ha-addon-repo (push) Has been cancelled
Publish Release / deploy-esphome-schema (push) Has been cancelled
Publish Release / version-notifier (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04-arm) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04-arm) (push) Has been cancelled
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
Stale / stale (push) Has been cancelled
Lock closed issues and PRs / lock (push) Has been cancelled
Publish Release / Initialize build (push) Has been cancelled
Publish Release / Build and publish to PyPi (push) Has been cancelled
Publish Release / Build ESPHome amd64 (push) Has been cancelled
Publish Release / Build ESPHome arm64 (push) Has been cancelled
Publish Release / Publish ESPHome docker to dockerhub (push) Has been cancelled
Publish Release / Publish ESPHome docker to ghcr (push) Has been cancelled
Publish Release / Publish ESPHome ha-addon to dockerhub (push) Has been cancelled
Publish Release / Publish ESPHome ha-addon to ghcr (push) Has been cancelled
Publish Release / deploy-ha-addon-repo (push) Has been cancelled
Publish Release / deploy-esphome-schema (push) Has been cancelled
Publish Release / version-notifier (push) Has been cancelled
Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
J. Nick Koston
J. Nick Koston
parent
75c41b11d1
commit
f20aaf3981
File diff suppressed because it is too large
Load Diff
@@ -855,6 +855,14 @@ enum ServiceArgType {
|
||||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
|
||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
|
||||
}
|
||||
enum SupportsResponseType {
|
||||
SUPPORTS_RESPONSE_NONE = 0;
|
||||
SUPPORTS_RESPONSE_OPTIONAL = 1;
|
||||
SUPPORTS_RESPONSE_ONLY = 2;
|
||||
// Status-only response - reports success/error without data payload
|
||||
// Value is higher to avoid conflicts with future Home Assistant values
|
||||
SUPPORTS_RESPONSE_STATUS = 100;
|
||||
}
|
||||
message ListEntitiesServicesArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
string name = 1;
|
||||
@@ -868,6 +876,7 @@ message ListEntitiesServicesResponse {
|
||||
string name = 1;
|
||||
fixed32 key = 2;
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
SupportsResponseType supports_response = 4;
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
@@ -890,6 +899,21 @@ message ExecuteServiceRequest {
|
||||
|
||||
fixed32 key = 1;
|
||||
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
|
||||
uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
|
||||
bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
|
||||
}
|
||||
|
||||
// Message sent by ESPHome to Home Assistant with service execution response data
|
||||
message ExecuteServiceResponse {
|
||||
option (id) = 131;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES";
|
||||
|
||||
uint32 call_id = 1; // Matches the call_id from ExecuteServiceRequest
|
||||
bool success = 2; // Whether the service execution succeeded
|
||||
string error_message = 3; // Error message if success = false
|
||||
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES_JSON"];
|
||||
}
|
||||
|
||||
// ==================== CAMERA ====================
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
#include "api_frame_helper_plaintext.h"
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
#include <cerrno>
|
||||
#include <cinttypes>
|
||||
#include <functional>
|
||||
@@ -1554,15 +1557,54 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
bool found = false;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Register the call and get a unique server-generated action_call_id
|
||||
// This avoids collisions when multiple clients use the same call_id
|
||||
uint32_t action_call_id = 0;
|
||||
if (msg.call_id != 0) {
|
||||
action_call_id = this->parent_->register_active_action_call(msg.call_id, this);
|
||||
}
|
||||
// Use the overload that passes action_call_id separately (avoids copying msg)
|
||||
for (auto *service : this->parent_->get_user_services()) {
|
||||
if (service->execute_service(msg, action_call_id)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
for (auto *service : this->parent_->get_user_services()) {
|
||||
if (service->execute_service(msg)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!found) {
|
||||
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.
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message) {
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(StringRef(error_message));
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(StringRef(error_message));
|
||||
resp.response_data = response_data;
|
||||
resp.response_data_len = response_data_len;
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
|
||||
@@ -223,6 +223,13 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
|
||||
@@ -1010,11 +1010,13 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
for (auto &it : this->args) {
|
||||
buffer.encode_message(3, it, true);
|
||||
}
|
||||
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
|
||||
}
|
||||
void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_length(1, this->name_ref_.size());
|
||||
size.add_fixed32(1, this->key);
|
||||
size.add_repeated_message(1, this->args);
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->supports_response));
|
||||
}
|
||||
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
@@ -1075,6 +1077,23 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
||||
this->string_array.init(count_string_array);
|
||||
ProtoDecodableMessage::decode(buffer, length);
|
||||
}
|
||||
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
case 3:
|
||||
this->call_id = value.as_uint32();
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
case 4:
|
||||
this->return_response = value.as_bool();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2:
|
||||
@@ -1102,6 +1121,24 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
|
||||
ProtoDecodableMessage::decode(buffer, length);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(1, this->call_id);
|
||||
buffer.encode_bool(2, this->success);
|
||||
buffer.encode_string(3, this->error_message_ref_);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
buffer.encode_bytes(4, this->response_data, this->response_data_len);
|
||||
#endif
|
||||
}
|
||||
void ExecuteServiceResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint32(1, this->call_id);
|
||||
size.add_bool(1, this->success);
|
||||
size.add_length(1, this->error_message_ref_.size());
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
size.add_length(4, this->response_data_len);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->object_id_ref_);
|
||||
|
||||
@@ -75,6 +75,12 @@ enum ServiceArgType : uint32_t {
|
||||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
|
||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7,
|
||||
};
|
||||
enum SupportsResponseType : uint32_t {
|
||||
SUPPORTS_RESPONSE_NONE = 0,
|
||||
SUPPORTS_RESPONSE_OPTIONAL = 1,
|
||||
SUPPORTS_RESPONSE_ONLY = 2,
|
||||
SUPPORTS_RESPONSE_STATUS = 100,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
enum ClimateMode : uint32_t {
|
||||
@@ -1257,7 +1263,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 41;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 48;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 50;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_services_response"; }
|
||||
#endif
|
||||
@@ -1265,6 +1271,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
uint32_t key{0};
|
||||
FixedVector<ListEntitiesServicesArgument> args{};
|
||||
enums::SupportsResponseType supports_response{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1297,12 +1304,18 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
|
||||
class ExecuteServiceRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 42;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 39;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 45;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "execute_service_request"; }
|
||||
#endif
|
||||
uint32_t key{0};
|
||||
FixedVector<ExecuteServiceArgument> args{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
uint32_t call_id{0};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
bool return_response{false};
|
||||
#endif
|
||||
void decode(const uint8_t *buffer, size_t length) override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
@@ -1311,6 +1324,32 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
class ExecuteServiceResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 131;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 34;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "execute_service_response"; }
|
||||
#endif
|
||||
uint32_t call_id{0};
|
||||
bool success{false};
|
||||
StringRef error_message_ref_{};
|
||||
void set_error_message(const StringRef &ref) { this->error_message_ref_ = ref; }
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
const uint8_t *response_data{nullptr};
|
||||
uint16_t response_data_len{0};
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
|
||||
@@ -231,6 +231,20 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::SupportsResponseType value) {
|
||||
switch (value) {
|
||||
case enums::SUPPORTS_RESPONSE_NONE:
|
||||
return "SUPPORTS_RESPONSE_NONE";
|
||||
case enums::SUPPORTS_RESPONSE_OPTIONAL:
|
||||
return "SUPPORTS_RESPONSE_OPTIONAL";
|
||||
case enums::SUPPORTS_RESPONSE_ONLY:
|
||||
return "SUPPORTS_RESPONSE_ONLY";
|
||||
case enums::SUPPORTS_RESPONSE_STATUS:
|
||||
return "SUPPORTS_RESPONSE_STATUS";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
|
||||
@@ -1194,6 +1208,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
dump_field(out, "supports_response", static_cast<enums::SupportsResponseType>(this->supports_response));
|
||||
}
|
||||
void ExecuteServiceArgument::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "ExecuteServiceArgument");
|
||||
@@ -1223,6 +1238,25 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
dump_field(out, "call_id", this->call_id);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
dump_field(out, "return_response", this->return_response);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void ExecuteServiceResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "ExecuteServiceResponse");
|
||||
dump_field(out, "call_id", this->call_id);
|
||||
dump_field(out, "success", this->success);
|
||||
dump_field(out, "error_message", this->error_message_ref_);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
out.append(" response_data: ");
|
||||
out.append(format_hex_pretty(this->response_data, this->response_data_len));
|
||||
out.append("\n");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include "api_connection.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
@@ -186,6 +186,9 @@ void APIServer::loop() {
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
|
||||
|
||||
@@ -585,5 +588,84 @@ bool APIServer::teardown() {
|
||||
return this->clients_.empty();
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
|
||||
// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
|
||||
#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
|
||||
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
|
||||
#endif
|
||||
|
||||
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
|
||||
uint32_t action_call_id = this->next_action_call_id_++;
|
||||
// Handle wraparound (skip 0 as it means "no call")
|
||||
if (this->next_action_call_id_ == 0) {
|
||||
this->next_action_call_id_ = 1;
|
||||
}
|
||||
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
|
||||
|
||||
// Schedule automatic cleanup after timeout (client will have given up by then)
|
||||
this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS,
|
||||
[this, action_call_id]() {
|
||||
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
|
||||
this->unregister_active_action_call(action_call_id);
|
||||
});
|
||||
|
||||
return action_call_id;
|
||||
}
|
||||
|
||||
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
|
||||
// Cancel the timeout for this action call
|
||||
this->cancel_timeout(str_sprintf("action_call_%u", action_call_id));
|
||||
|
||||
// Swap-and-pop is more efficient than remove_if for unordered vectors
|
||||
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
|
||||
if (this->active_action_calls_[i].action_call_id == action_call_id) {
|
||||
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
||||
this->active_action_calls_.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) {
|
||||
// Remove all active action calls for disconnected connection using swap-and-pop
|
||||
for (size_t i = 0; i < this->active_action_calls_.size();) {
|
||||
if (this->active_action_calls_[i].connection == conn) {
|
||||
// Cancel the timeout for this action call
|
||||
this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id));
|
||||
|
||||
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
||||
this->active_action_calls_.pop_back();
|
||||
// Don't increment i - need to check the swapped element
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message) {
|
||||
for (auto &call : this->active_action_calls_) {
|
||||
if (call.action_call_id == action_call_id) {
|
||||
call.connection->send_execute_service_response(call.client_call_id, success, error_message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
for (auto &call : this->active_action_calls_) {
|
||||
if (call.action_call_id == action_call_id) {
|
||||
call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
|
||||
response_data_len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
}
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "list_entities.h"
|
||||
#include "subscribe_state.h"
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
@@ -22,11 +19,15 @@
|
||||
#include "esphome/components/camera/camera.h"
|
||||
#endif
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
// Forward declaration - full definition in user_services.h
|
||||
class UserServiceDescriptor;
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
struct SavedNoisePsk {
|
||||
psk_t psk;
|
||||
@@ -154,6 +155,19 @@ class APIServer : public Component,
|
||||
// Only compile push_back method when custom_services: true (external components)
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Action call context management - supports concurrent calls from multiple clients
|
||||
// Returns server-generated action_call_id to avoid collisions when clients use same call_id
|
||||
uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn);
|
||||
void unregister_active_action_call(uint32_t action_call_id);
|
||||
void unregister_active_action_calls_for_connection(APIConnection *conn);
|
||||
// Send response for a specific action call (uses action_call_id, sends client_call_id in response)
|
||||
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void request_time();
|
||||
@@ -230,6 +244,17 @@ class APIServer : public Component,
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
std::vector<UserServiceDescriptor *> user_services_;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Active action calls - supports concurrent calls from multiple clients
|
||||
// Uses server-generated action_call_id to avoid collisions when multiple clients use same call_id
|
||||
struct ActiveActionCall {
|
||||
uint32_t action_call_id; // Server-generated unique ID (passed to actions)
|
||||
uint32_t client_call_id; // Client's original call_id (used in response)
|
||||
APIConnection *connection;
|
||||
};
|
||||
std::vector<ActiveActionCall> active_action_calls_;
|
||||
uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
struct PendingActionResponse {
|
||||
|
||||
@@ -16,7 +16,10 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
|
||||
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
||||
|
||||
protected:
|
||||
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
||||
// CustomAPIDevice services don't support action responses - ignore call_id and return_response
|
||||
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override {
|
||||
(this->obj_->*this->callback_)(x...); // NOLINT
|
||||
}
|
||||
|
||||
T *obj_;
|
||||
void (T::*callback_)(Ts...);
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -129,6 +129,8 @@
|
||||
#define USE_API_PLAINTEXT
|
||||
#define USE_API_USER_DEFINED_ACTIONS
|
||||
#define USE_API_CUSTOM_SERVICES
|
||||
#define USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#define USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#define API_MAX_SEND_QUEUE 8
|
||||
#define USE_MD5
|
||||
#define USE_SHA256
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==42.10.0
|
||||
aioesphomeapi==43.0.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.16 # dashboard_import
|
||||
|
||||
@@ -181,6 +181,99 @@ api:
|
||||
else:
|
||||
- logger.log: "Skipped loops"
|
||||
- logger.log: "After combined test"
|
||||
# ==========================================================================
|
||||
# supports_response: status (auto-detected - api.respond without data)
|
||||
# Has call_id only - reports success/error without data payload
|
||||
# ==========================================================================
|
||||
- action: test_respond_status
|
||||
then:
|
||||
- api.respond:
|
||||
success: true
|
||||
- logger.log:
|
||||
format: "Status response sent (call_id=%d)"
|
||||
args: [call_id]
|
||||
|
||||
- action: test_respond_status_error
|
||||
variables:
|
||||
error_msg: string
|
||||
then:
|
||||
- api.respond:
|
||||
success: false
|
||||
error_message: !lambda 'return error_msg;'
|
||||
|
||||
# ==========================================================================
|
||||
# supports_response: optional (auto-detected - api.respond with data)
|
||||
# Has call_id and return_response - client decides if it wants response
|
||||
# ==========================================================================
|
||||
- action: test_respond_optional
|
||||
variables:
|
||||
sensor_name: string
|
||||
value: float
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Optional response (call_id=%d, return_response=%d)"
|
||||
args: [call_id, return_response]
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["sensor"] = sensor_name;
|
||||
root["value"] = value;
|
||||
root["unit"] = "°C";
|
||||
|
||||
- action: test_respond_optional_conditional
|
||||
variables:
|
||||
do_succeed: bool
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return do_succeed;'
|
||||
then:
|
||||
- api.respond:
|
||||
success: true
|
||||
data: !lambda |-
|
||||
root["status"] = "ok";
|
||||
else:
|
||||
- api.respond:
|
||||
success: false
|
||||
error_message: "Operation failed"
|
||||
|
||||
# ==========================================================================
|
||||
# supports_response: only (explicit - always expects data response)
|
||||
# Has call_id only - response is always expected with data
|
||||
# ==========================================================================
|
||||
- action: test_respond_only
|
||||
supports_response: only
|
||||
variables:
|
||||
input: string
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Only response (call_id=%d)"
|
||||
args: [call_id]
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["input"] = input;
|
||||
root["processed"] = true;
|
||||
|
||||
- action: test_respond_only_nested
|
||||
supports_response: only
|
||||
then:
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["config"]["wifi"] = "connected";
|
||||
root["config"]["api"] = true;
|
||||
root["items"][0] = "item1";
|
||||
root["items"][1] = "item2";
|
||||
|
||||
# ==========================================================================
|
||||
# supports_response: none (no api.respond action)
|
||||
# No call_id or return_response - just user variables
|
||||
# ==========================================================================
|
||||
- action: test_no_response
|
||||
variables:
|
||||
message: string
|
||||
then:
|
||||
- logger.log:
|
||||
format: "No response action: %s"
|
||||
args: [message.c_str()]
|
||||
|
||||
event:
|
||||
- platform: template
|
||||
|
||||
@@ -252,7 +252,7 @@ my_service = next((s for s in services if s.name == "my_service"), None)
|
||||
assert my_service is not None
|
||||
|
||||
# Execute with parameters
|
||||
client.execute_service(my_service, {"param1": "value1", "param2": 42})
|
||||
await client.execute_service(my_service, {"param1": "value1", "param2": 42})
|
||||
```
|
||||
|
||||
##### Multiple Entity Tracking
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
esphome:
|
||||
name: api-action-responses-test
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
actions:
|
||||
# ==========================================================================
|
||||
# supports_response: none (default - no api.respond action)
|
||||
# No call_id or return_response - just user variables
|
||||
# ==========================================================================
|
||||
- action: action_no_response
|
||||
variables:
|
||||
message: string
|
||||
then:
|
||||
- logger.log:
|
||||
format: "ACTION_NO_RESPONSE called with: %s"
|
||||
args: [message.c_str()]
|
||||
|
||||
# ==========================================================================
|
||||
# supports_response: status (auto-detected - api.respond without data)
|
||||
# Has call_id only - reports success/error without data payload
|
||||
# ==========================================================================
|
||||
- action: action_status_response
|
||||
variables:
|
||||
should_succeed: bool
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return should_succeed;'
|
||||
then:
|
||||
- api.respond:
|
||||
success: true
|
||||
- logger.log:
|
||||
format: "ACTION_STATUS_RESPONSE success (call_id=%d)"
|
||||
args: [call_id]
|
||||
else:
|
||||
- api.respond:
|
||||
success: false
|
||||
error_message: "Intentional failure for testing"
|
||||
- logger.log:
|
||||
format: "ACTION_STATUS_RESPONSE error (call_id=%d)"
|
||||
args: [call_id]
|
||||
|
||||
# ==========================================================================
|
||||
# supports_response: optional (auto-detected - api.respond with data)
|
||||
# Has call_id and return_response - client decides if it wants response
|
||||
# ==========================================================================
|
||||
- action: action_optional_response
|
||||
variables:
|
||||
value: int
|
||||
then:
|
||||
- logger.log:
|
||||
format: "ACTION_OPTIONAL_RESPONSE (call_id=%d, return_response=%d, value=%d)"
|
||||
args: [call_id, return_response, value]
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["input"] = value;
|
||||
root["doubled"] = value * 2;
|
||||
|
||||
# ==========================================================================
|
||||
# supports_response: only (explicit - always expects data response)
|
||||
# Has call_id only - response is always expected with data
|
||||
# ==========================================================================
|
||||
- action: action_only_response
|
||||
supports_response: only
|
||||
variables:
|
||||
name: string
|
||||
then:
|
||||
- logger.log:
|
||||
format: "ACTION_ONLY_RESPONSE (call_id=%d, name=%s)"
|
||||
args: [call_id, name.c_str()]
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["greeting"] = "Hello, " + name + "!";
|
||||
root["length"] = name.length();
|
||||
|
||||
# Test action with nested JSON response
|
||||
- action: action_nested_json
|
||||
supports_response: only
|
||||
then:
|
||||
- logger.log:
|
||||
format: "ACTION_NESTED_JSON (call_id=%d)"
|
||||
args: [call_id]
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["config"]["wifi"]["connected"] = true;
|
||||
root["config"]["api"]["port"] = 6053;
|
||||
root["items"][0] = "first";
|
||||
root["items"][1] = "second";
|
||||
@@ -0,0 +1,45 @@
|
||||
esphome:
|
||||
name: api-action-timeout-test
|
||||
# Use a short timeout for testing (500ms instead of 30s)
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- "-DUSE_API_ACTION_CALL_TIMEOUT_MS=500"
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
actions:
|
||||
# Action that responds immediately - should work fine
|
||||
- action: action_immediate
|
||||
supports_response: only
|
||||
then:
|
||||
- logger.log: "ACTION_IMMEDIATE responding"
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["status"] = "immediate";
|
||||
|
||||
# Action that delays 200ms before responding - should work (within 500ms timeout)
|
||||
- action: action_short_delay
|
||||
supports_response: only
|
||||
then:
|
||||
- logger.log: "ACTION_SHORT_DELAY starting"
|
||||
- delay: 200ms
|
||||
- logger.log: "ACTION_SHORT_DELAY responding"
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["status"] = "short_delay";
|
||||
|
||||
# Action that delays 1s before responding - should fail (exceeds 500ms timeout)
|
||||
# The api.respond will log a warning because the action call was already cleaned up
|
||||
- action: action_long_delay
|
||||
supports_response: only
|
||||
then:
|
||||
- logger.log: "ACTION_LONG_DELAY starting"
|
||||
- delay: 1s
|
||||
- logger.log: "ACTION_LONG_DELAY responding (after timeout)"
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["status"] = "long_delay";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,172 @@
|
||||
"""Integration test for API action call timeout functionality.
|
||||
|
||||
Tests that action calls are automatically cleaned up after timeout,
|
||||
and that late responses are handled gracefully.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import re
|
||||
|
||||
from aioesphomeapi import UserService
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_action_timeout(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test API action call timeout behavior.
|
||||
|
||||
This test uses a 500ms timeout (set via USE_API_ACTION_CALL_TIMEOUT_MS define)
|
||||
to verify:
|
||||
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
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track log messages
|
||||
immediate_future = loop.create_future()
|
||||
short_delay_responding_future = loop.create_future()
|
||||
long_delay_starting_future = loop.create_future()
|
||||
long_delay_responding_future = loop.create_future()
|
||||
timeout_warning_future = loop.create_future()
|
||||
|
||||
# Patterns to match in logs
|
||||
immediate_pattern = re.compile(r"ACTION_IMMEDIATE responding")
|
||||
short_delay_responding_pattern = re.compile(r"ACTION_SHORT_DELAY responding")
|
||||
long_delay_starting_pattern = re.compile(r"ACTION_LONG_DELAY starting")
|
||||
long_delay_responding_pattern = re.compile(
|
||||
r"ACTION_LONG_DELAY responding \(after timeout\)"
|
||||
)
|
||||
# This warning is logged when api.respond is called after the action call timed out
|
||||
timeout_warning_pattern = re.compile(
|
||||
r"Cannot send response: no active call found for action_call_id"
|
||||
)
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if not immediate_future.done() and immediate_pattern.search(line):
|
||||
immediate_future.set_result(True)
|
||||
elif (
|
||||
not short_delay_responding_future.done()
|
||||
and short_delay_responding_pattern.search(line)
|
||||
):
|
||||
short_delay_responding_future.set_result(True)
|
||||
elif (
|
||||
not long_delay_starting_future.done()
|
||||
and long_delay_starting_pattern.search(line)
|
||||
):
|
||||
long_delay_starting_future.set_result(True)
|
||||
elif (
|
||||
not long_delay_responding_future.done()
|
||||
and long_delay_responding_pattern.search(line)
|
||||
):
|
||||
long_delay_responding_future.set_result(True)
|
||||
elif not timeout_warning_future.done() and timeout_warning_pattern.search(line):
|
||||
timeout_warning_future.set_result(True)
|
||||
|
||||
# Run with log monitoring
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "api-action-timeout-test"
|
||||
|
||||
# List services
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Should have 3 services
|
||||
assert len(services) == 3, f"Expected 3 services, found {len(services)}"
|
||||
|
||||
# Find our services
|
||||
action_immediate: UserService | None = None
|
||||
action_short_delay: UserService | None = None
|
||||
action_long_delay: UserService | None = None
|
||||
|
||||
for service in services:
|
||||
if service.name == "action_immediate":
|
||||
action_immediate = service
|
||||
elif service.name == "action_short_delay":
|
||||
action_short_delay = service
|
||||
elif service.name == "action_long_delay":
|
||||
action_long_delay = service
|
||||
|
||||
assert action_immediate is not None, "action_immediate not found"
|
||||
assert action_short_delay is not None, "action_short_delay not found"
|
||||
assert action_long_delay is not None, "action_long_delay not found"
|
||||
|
||||
# Test 1: Immediate response should work
|
||||
response = await client.execute_service(
|
||||
action_immediate,
|
||||
{},
|
||||
return_response=True,
|
||||
)
|
||||
await asyncio.wait_for(immediate_future, timeout=1.0)
|
||||
assert response is not None, "Expected response for immediate action"
|
||||
assert response.success is True
|
||||
|
||||
# Test 2: Short delay (200ms) should work within the 500ms timeout
|
||||
response = await client.execute_service(
|
||||
action_short_delay,
|
||||
{},
|
||||
return_response=True,
|
||||
)
|
||||
await asyncio.wait_for(short_delay_responding_future, timeout=1.0)
|
||||
assert response is not None, "Expected response for short delay action"
|
||||
assert response.success is True
|
||||
|
||||
# Test 3: Long delay (1s) should exceed the 500ms timeout
|
||||
# The server-side timeout will clean up the action call after 500ms
|
||||
# The client will timeout waiting for the response
|
||||
# When the action finally tries to respond after 1s, it will log a warning
|
||||
|
||||
# Start the long delay action (don't await it fully - it will timeout)
|
||||
long_delay_task = asyncio.create_task(
|
||||
client.execute_service(
|
||||
action_long_delay,
|
||||
{},
|
||||
return_response=True,
|
||||
timeout=2.0, # Give client enough time to see the late response attempt
|
||||
)
|
||||
)
|
||||
|
||||
# Wait for the action to start
|
||||
await asyncio.wait_for(long_delay_starting_future, timeout=1.0)
|
||||
|
||||
# Wait for the action to try to respond (after 1s delay)
|
||||
await asyncio.wait_for(long_delay_responding_future, timeout=2.0)
|
||||
|
||||
# Wait for the warning log about no active call
|
||||
await asyncio.wait_for(timeout_warning_future, timeout=1.0)
|
||||
|
||||
# The client task should complete (either with None response or timeout)
|
||||
# Client timing out is acceptable - the server-side timeout already cleaned up the call
|
||||
with contextlib.suppress(TimeoutError):
|
||||
await asyncio.wait_for(long_delay_task, timeout=1.0)
|
||||
|
||||
# Verify the system is still functional after the timeout
|
||||
# Call the immediate action again to prove cleanup worked
|
||||
immediate_future_2 = loop.create_future()
|
||||
|
||||
def check_output_2(line: str) -> None:
|
||||
if not immediate_future_2.done() and immediate_pattern.search(line):
|
||||
immediate_future_2.set_result(True)
|
||||
|
||||
response = await client.execute_service(
|
||||
action_immediate,
|
||||
{},
|
||||
return_response=True,
|
||||
)
|
||||
assert response is not None, "System should still work after timeout"
|
||||
assert response.success is True
|
||||
@@ -88,13 +88,13 @@ async def test_api_conditional_memory(
|
||||
assert arg_types["arg_float"] == UserServiceArgType.FLOAT
|
||||
|
||||
# Call simple service
|
||||
client.execute_service(simple_service, {})
|
||||
await client.execute_service(simple_service, {})
|
||||
|
||||
# Wait for service log
|
||||
await asyncio.wait_for(service_simple_future, timeout=5.0)
|
||||
|
||||
# Call service with arguments
|
||||
client.execute_service(
|
||||
await client.execute_service(
|
||||
service_with_args,
|
||||
{
|
||||
"arg_string": "test_string",
|
||||
|
||||
@@ -114,7 +114,7 @@ async def test_api_custom_services(
|
||||
assert custom_arrays_service is not None, "custom_service_with_arrays not found"
|
||||
|
||||
# Test YAML service
|
||||
client.execute_service(yaml_service, {})
|
||||
await client.execute_service(yaml_service, {})
|
||||
await asyncio.wait_for(yaml_service_future, timeout=5.0)
|
||||
|
||||
# Verify YAML service with args arguments
|
||||
@@ -124,7 +124,7 @@ async def test_api_custom_services(
|
||||
assert yaml_args_types["my_string"] == UserServiceArgType.STRING
|
||||
|
||||
# Test YAML service with arguments
|
||||
client.execute_service(
|
||||
await client.execute_service(
|
||||
yaml_args_service,
|
||||
{
|
||||
"my_int": 123,
|
||||
@@ -144,7 +144,7 @@ async def test_api_custom_services(
|
||||
assert yaml_many_args_types["arg4"] == UserServiceArgType.STRING
|
||||
|
||||
# Test YAML service with many arguments
|
||||
client.execute_service(
|
||||
await client.execute_service(
|
||||
yaml_many_args_service,
|
||||
{
|
||||
"arg1": 42,
|
||||
@@ -156,7 +156,7 @@ async def test_api_custom_services(
|
||||
await asyncio.wait_for(yaml_many_args_future, timeout=5.0)
|
||||
|
||||
# Test simple CustomAPIDevice service
|
||||
client.execute_service(custom_service, {})
|
||||
await client.execute_service(custom_service, {})
|
||||
await asyncio.wait_for(custom_service_future, timeout=5.0)
|
||||
|
||||
# Verify custom_args_service arguments
|
||||
@@ -168,7 +168,7 @@ async def test_api_custom_services(
|
||||
assert arg_types["arg_float"] == UserServiceArgType.FLOAT
|
||||
|
||||
# Test CustomAPIDevice service with arguments
|
||||
client.execute_service(
|
||||
await client.execute_service(
|
||||
custom_args_service,
|
||||
{
|
||||
"arg_string": "test_string",
|
||||
@@ -188,7 +188,7 @@ async def test_api_custom_services(
|
||||
assert array_arg_types["string_array"] == UserServiceArgType.STRING_ARRAY
|
||||
|
||||
# Test CustomAPIDevice service with arrays
|
||||
client.execute_service(
|
||||
await client.execute_service(
|
||||
custom_arrays_service,
|
||||
{
|
||||
"bool_array": [True, False],
|
||||
|
||||
@@ -163,7 +163,7 @@ async def test_api_homeassistant(
|
||||
assert trigger_service is not None, "trigger_all_tests service not found"
|
||||
|
||||
# Execute all tests
|
||||
client.execute_service(trigger_service, {})
|
||||
await client.execute_service(trigger_service, {})
|
||||
|
||||
# Wait for all tests to complete with appropriate timeouts
|
||||
try:
|
||||
|
||||
@@ -75,10 +75,12 @@ async def test_api_string_lambda(
|
||||
assert char_ptr_service is not None, "test_char_ptr_lambda service not found"
|
||||
|
||||
# Execute all four services to test different lambda return types
|
||||
client.execute_service(string_service, {"input_string": "STRING_FROM_LAMBDA"})
|
||||
client.execute_service(int_service, {"input_number": 42})
|
||||
client.execute_service(float_service, {"input_float": 3.14})
|
||||
client.execute_service(
|
||||
await client.execute_service(
|
||||
string_service, {"input_string": "STRING_FROM_LAMBDA"}
|
||||
)
|
||||
await client.execute_service(int_service, {"input_number": 42})
|
||||
await client.execute_service(float_service, {"input_float": 3.14})
|
||||
await client.execute_service(
|
||||
char_ptr_service, {"input_number": 123, "input_string": "test_string"}
|
||||
)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ async def test_automation_wait_actions(
|
||||
# Test 1: wait_until in automation - trigger 5 times rapidly
|
||||
test_service = next((s for s in services if s.name == "test_wait_until"), None)
|
||||
assert test_service is not None, "test_wait_until service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test1_complete, timeout=3.0)
|
||||
|
||||
# Verify Test 1: All 5 triggers should complete
|
||||
@@ -82,7 +82,7 @@ async def test_automation_wait_actions(
|
||||
# Test 2: script.wait in automation - trigger 5 times rapidly
|
||||
test_service = next((s for s in services if s.name == "test_script_wait"), None)
|
||||
assert test_service is not None, "test_script_wait service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test2_complete, timeout=3.0)
|
||||
|
||||
# Verify Test 2: All 5 triggers should complete
|
||||
@@ -95,7 +95,7 @@ async def test_automation_wait_actions(
|
||||
(s for s in services if s.name == "test_wait_timeout"), None
|
||||
)
|
||||
assert test_service is not None, "test_wait_timeout service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test3_complete, timeout=3.0)
|
||||
|
||||
# Verify Test 3: All 5 triggers should timeout and complete
|
||||
|
||||
@@ -67,7 +67,7 @@ async def test_delay_action_cancellation(
|
||||
assert test_service is not None, "start_delay_then_restart service not found"
|
||||
|
||||
# Execute the test sequence
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
|
||||
# Wait for the second script to start
|
||||
await asyncio.wait_for(second_script_started, timeout=5.0)
|
||||
@@ -138,7 +138,7 @@ async def test_parallel_script_delays(
|
||||
assert test_service is not None, "test_parallel_delays service not found"
|
||||
|
||||
# Execute the test - this will start 3 parallel scripts with 1 second delays
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
|
||||
# Wait for all scripts to complete (should take ~1 second, not 3)
|
||||
await asyncio.wait_for(all_scripts_completed, timeout=2.0)
|
||||
|
||||
@@ -142,7 +142,7 @@ async def test_continuation_actions(
|
||||
# Test 1: IfAction with then branch
|
||||
test_service = next((s for s in services if s.name == "test_if_action"), None)
|
||||
assert test_service is not None, "test_if_action service not found"
|
||||
client.execute_service(test_service, {"condition": True, "value": 42})
|
||||
await client.execute_service(test_service, {"condition": True, "value": 42})
|
||||
await asyncio.wait_for(test1_complete, timeout=2.0)
|
||||
assert test_results["if_then"], "IfAction then branch not executed"
|
||||
assert test_results["if_complete"], "IfAction did not complete"
|
||||
@@ -150,7 +150,7 @@ async def test_continuation_actions(
|
||||
# Test 1b: IfAction with else branch
|
||||
test1_complete = loop.create_future()
|
||||
test_results["if_complete"] = False
|
||||
client.execute_service(test_service, {"condition": False, "value": 99})
|
||||
await client.execute_service(test_service, {"condition": False, "value": 99})
|
||||
await asyncio.wait_for(test1_complete, timeout=2.0)
|
||||
assert test_results["if_else"], "IfAction else branch not executed"
|
||||
assert test_results["if_complete"], "IfAction did not complete"
|
||||
@@ -160,14 +160,14 @@ async def test_continuation_actions(
|
||||
assert test_service is not None, "test_nested_if service not found"
|
||||
|
||||
# Both true
|
||||
client.execute_service(test_service, {"outer": True, "inner": True})
|
||||
await client.execute_service(test_service, {"outer": True, "inner": True})
|
||||
await asyncio.wait_for(test2_complete, timeout=2.0)
|
||||
assert test_results["nested_both_true"], "Nested both true not executed"
|
||||
|
||||
# Outer true, inner false
|
||||
test2_complete = loop.create_future()
|
||||
test_results["nested_complete"] = False
|
||||
client.execute_service(test_service, {"outer": True, "inner": False})
|
||||
await client.execute_service(test_service, {"outer": True, "inner": False})
|
||||
await asyncio.wait_for(test2_complete, timeout=2.0)
|
||||
assert test_results["nested_outer_true_inner_false"], (
|
||||
"Nested outer true inner false not executed"
|
||||
@@ -176,7 +176,7 @@ async def test_continuation_actions(
|
||||
# Outer false
|
||||
test2_complete = loop.create_future()
|
||||
test_results["nested_complete"] = False
|
||||
client.execute_service(test_service, {"outer": False, "inner": True})
|
||||
await client.execute_service(test_service, {"outer": False, "inner": True})
|
||||
await asyncio.wait_for(test2_complete, timeout=2.0)
|
||||
assert test_results["nested_outer_false"], "Nested outer false not executed"
|
||||
|
||||
@@ -185,7 +185,7 @@ async def test_continuation_actions(
|
||||
(s for s in services if s.name == "test_while_action"), None
|
||||
)
|
||||
assert test_service is not None, "test_while_action service not found"
|
||||
client.execute_service(test_service, {"max_count": 3})
|
||||
await client.execute_service(test_service, {"max_count": 3})
|
||||
await asyncio.wait_for(test3_complete, timeout=2.0)
|
||||
assert test_results["while_iterations"] == 3, (
|
||||
f"WhileAction expected 3 iterations, got {test_results['while_iterations']}"
|
||||
@@ -197,7 +197,7 @@ async def test_continuation_actions(
|
||||
(s for s in services if s.name == "test_repeat_action"), None
|
||||
)
|
||||
assert test_service is not None, "test_repeat_action service not found"
|
||||
client.execute_service(test_service, {"count": 5})
|
||||
await client.execute_service(test_service, {"count": 5})
|
||||
await asyncio.wait_for(test4_complete, timeout=2.0)
|
||||
assert test_results["repeat_iterations"] == 5, (
|
||||
f"RepeatAction expected 5 iterations, got {test_results['repeat_iterations']}"
|
||||
@@ -207,7 +207,7 @@ async def test_continuation_actions(
|
||||
# Test 5: Combined (if + repeat + while)
|
||||
test_service = next((s for s in services if s.name == "test_combined"), None)
|
||||
assert test_service is not None, "test_combined service not found"
|
||||
client.execute_service(test_service, {"do_loop": True, "loop_count": 2})
|
||||
await client.execute_service(test_service, {"do_loop": True, "loop_count": 2})
|
||||
await asyncio.wait_for(test5_complete, timeout=2.0)
|
||||
# Should execute: repeat 2 times, each iteration does while from iteration down to 0
|
||||
# iteration 0: while 0 times = 0
|
||||
@@ -221,7 +221,7 @@ async def test_continuation_actions(
|
||||
# Test 6: Rapid triggers (tests memory efficiency of ContinuationAction)
|
||||
test_service = next((s for s in services if s.name == "test_rapid_if"), None)
|
||||
assert test_service is not None, "test_rapid_if service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test6_complete, timeout=2.0)
|
||||
# Values 1, 2 should hit else (<=2), values 3, 4, 5 should hit then (>2)
|
||||
assert test_results["rapid_else"] == 2, (
|
||||
|
||||
@@ -98,7 +98,7 @@ async def test_scheduler_bulk_cleanup(
|
||||
)
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(trigger_bulk_cleanup_service, {})
|
||||
await client.execute_service(trigger_bulk_cleanup_service, {})
|
||||
|
||||
# Wait for test completion
|
||||
try:
|
||||
|
||||
@@ -81,7 +81,7 @@ async def test_scheduler_defer_cancel(
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(test_defer_cancel_service, {})
|
||||
await client.execute_service(test_defer_cancel_service, {})
|
||||
|
||||
# Wait for test completion
|
||||
try:
|
||||
|
||||
@@ -59,7 +59,7 @@ async def test_scheduler_defer_cancels_regular(
|
||||
assert test_service is not None, "test_defer_cancels_regular service not found"
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
|
||||
# Wait for test completion
|
||||
try:
|
||||
|
||||
@@ -84,7 +84,7 @@ async def test_scheduler_defer_fifo_simple(
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Test 1: Test set_timeout(0)
|
||||
client.execute_service(test_set_timeout_service, {})
|
||||
await client.execute_service(test_set_timeout_service, {})
|
||||
|
||||
# Wait for first test completion
|
||||
try:
|
||||
@@ -102,7 +102,7 @@ async def test_scheduler_defer_fifo_simple(
|
||||
test_result_future = loop.create_future()
|
||||
|
||||
# Test 2: Test defer()
|
||||
client.execute_service(test_defer_service, {})
|
||||
await client.execute_service(test_defer_service, {})
|
||||
|
||||
# Wait for second test completion
|
||||
try:
|
||||
|
||||
@@ -92,7 +92,7 @@ async def test_scheduler_defer_stress(
|
||||
assert run_stress_test_service is not None, "run_stress_test service not found"
|
||||
|
||||
# Call the run_stress_test service to start the test
|
||||
client.execute_service(run_stress_test_service, {})
|
||||
await client.execute_service(run_stress_test_service, {})
|
||||
|
||||
# Wait for all defers to execute (should be quick)
|
||||
try:
|
||||
|
||||
@@ -99,7 +99,7 @@ async def test_scheduler_heap_stress(
|
||||
)
|
||||
|
||||
# Call the run_heap_stress_test service to start the test
|
||||
client.execute_service(run_stress_test_service, {})
|
||||
await client.execute_service(run_stress_test_service, {})
|
||||
|
||||
# Wait for all callbacks to execute (should be quick, but give more time for scheduling)
|
||||
try:
|
||||
|
||||
@@ -48,7 +48,7 @@ async def test_scheduler_null_name(
|
||||
assert test_null_name_service is not None, "test_null_name service not found"
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(test_null_name_service, {})
|
||||
await client.execute_service(test_null_name_service, {})
|
||||
|
||||
# Wait for test completion
|
||||
try:
|
||||
|
||||
@@ -120,42 +120,42 @@ async def test_scheduler_pool(
|
||||
|
||||
try:
|
||||
# Phase 1: Component lifecycle
|
||||
client.execute_service(phase_services[1], {})
|
||||
await client.execute_service(phase_services[1], {})
|
||||
await asyncio.wait_for(phase_futures[1], timeout=1.0)
|
||||
await asyncio.sleep(0.05) # Let timeouts complete
|
||||
|
||||
# Phase 2: Sensor polling
|
||||
client.execute_service(phase_services[2], {})
|
||||
await client.execute_service(phase_services[2], {})
|
||||
await asyncio.wait_for(phase_futures[2], timeout=1.0)
|
||||
await asyncio.sleep(0.1) # Let intervals run a bit
|
||||
|
||||
# Phase 3: Communication patterns
|
||||
client.execute_service(phase_services[3], {})
|
||||
await client.execute_service(phase_services[3], {})
|
||||
await asyncio.wait_for(phase_futures[3], timeout=1.0)
|
||||
await asyncio.sleep(0.1) # Let heartbeat run
|
||||
|
||||
# Phase 4: Defer patterns
|
||||
client.execute_service(phase_services[4], {})
|
||||
await client.execute_service(phase_services[4], {})
|
||||
await asyncio.wait_for(phase_futures[4], timeout=1.0)
|
||||
await asyncio.sleep(0.2) # Let everything settle and recycle
|
||||
|
||||
# Phase 5: Pool reuse verification
|
||||
client.execute_service(phase_services[5], {})
|
||||
await client.execute_service(phase_services[5], {})
|
||||
await asyncio.wait_for(phase_futures[5], timeout=1.0)
|
||||
await asyncio.sleep(0.1) # Let Phase 5 timeouts complete and recycle
|
||||
|
||||
# Phase 6: Full pool reuse verification
|
||||
client.execute_service(phase_services[6], {})
|
||||
await client.execute_service(phase_services[6], {})
|
||||
await asyncio.wait_for(phase_futures[6], timeout=1.0)
|
||||
await asyncio.sleep(0.1) # Let Phase 6 timeouts complete
|
||||
|
||||
# Phase 7: Same-named defer optimization
|
||||
client.execute_service(phase_services[7], {})
|
||||
await client.execute_service(phase_services[7], {})
|
||||
await asyncio.wait_for(phase_futures[7], timeout=1.0)
|
||||
await asyncio.sleep(0.05) # Let the single defer execute
|
||||
|
||||
# Complete test
|
||||
client.execute_service(complete_service, {})
|
||||
await client.execute_service(complete_service, {})
|
||||
await asyncio.wait_for(test_complete_future, timeout=0.5)
|
||||
|
||||
except TimeoutError as e:
|
||||
|
||||
@@ -108,7 +108,7 @@ async def test_scheduler_rapid_cancellation(
|
||||
)
|
||||
|
||||
# Call the service to start the test
|
||||
client.execute_service(run_test_service, {})
|
||||
await client.execute_service(run_test_service, {})
|
||||
|
||||
# Wait for test to complete with timeout
|
||||
try:
|
||||
|
||||
@@ -79,7 +79,7 @@ async def test_scheduler_recursive_timeout(
|
||||
)
|
||||
|
||||
# Call the service to start the test
|
||||
client.execute_service(run_test_service, {})
|
||||
await client.execute_service(run_test_service, {})
|
||||
|
||||
# Wait for test to complete
|
||||
try:
|
||||
|
||||
@@ -81,7 +81,7 @@ async def test_scheduler_removed_item_race(
|
||||
assert run_test_service is not None, "run_test service not found"
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(run_test_service, {})
|
||||
await client.execute_service(run_test_service, {})
|
||||
|
||||
# Wait for test completion
|
||||
try:
|
||||
|
||||
@@ -98,7 +98,7 @@ async def test_scheduler_simultaneous_callbacks(
|
||||
)
|
||||
|
||||
# Call the service to start the test
|
||||
client.execute_service(run_test_service, {})
|
||||
await client.execute_service(run_test_service, {})
|
||||
|
||||
# Wait for test to complete
|
||||
try:
|
||||
|
||||
@@ -134,27 +134,27 @@ async def test_scheduler_string_lifetime(
|
||||
# Run tests sequentially, waiting for each to complete
|
||||
try:
|
||||
# Test 1
|
||||
client.execute_service(test_services["test1"], {})
|
||||
await client.execute_service(test_services["test1"], {})
|
||||
await asyncio.wait_for(test1_complete.wait(), timeout=5.0)
|
||||
|
||||
# Test 2
|
||||
client.execute_service(test_services["test2"], {})
|
||||
await client.execute_service(test_services["test2"], {})
|
||||
await asyncio.wait_for(test2_complete.wait(), timeout=5.0)
|
||||
|
||||
# Test 3
|
||||
client.execute_service(test_services["test3"], {})
|
||||
await client.execute_service(test_services["test3"], {})
|
||||
await asyncio.wait_for(test3_complete.wait(), timeout=5.0)
|
||||
|
||||
# Test 4
|
||||
client.execute_service(test_services["test4"], {})
|
||||
await client.execute_service(test_services["test4"], {})
|
||||
await asyncio.wait_for(test4_complete.wait(), timeout=5.0)
|
||||
|
||||
# Test 5
|
||||
client.execute_service(test_services["test5"], {})
|
||||
await client.execute_service(test_services["test5"], {})
|
||||
await asyncio.wait_for(test5_complete.wait(), timeout=5.0)
|
||||
|
||||
# Final check
|
||||
client.execute_service(test_services["final"], {})
|
||||
await client.execute_service(test_services["final"], {})
|
||||
await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0)
|
||||
|
||||
except TimeoutError:
|
||||
|
||||
@@ -92,7 +92,7 @@ async def test_scheduler_string_name_stress(
|
||||
)
|
||||
|
||||
# Call the service to start the test
|
||||
client.execute_service(run_stress_test_service, {})
|
||||
await client.execute_service(run_stress_test_service, {})
|
||||
|
||||
# Wait for test to complete or crash
|
||||
try:
|
||||
|
||||
@@ -90,7 +90,7 @@ async def test_script_delay_with_params(
|
||||
assert test_service is not None, "test_repeat_with_delay service not found"
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
|
||||
# Wait for test to complete (10 iterations * ~100ms each + margin)
|
||||
try:
|
||||
|
||||
@@ -136,7 +136,7 @@ async def test_script_queued(
|
||||
# Test 1: Queue depth limit
|
||||
test_service = next((s for s in services if s.name == "test_queue_depth"), None)
|
||||
assert test_service is not None, "test_queue_depth service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test1_complete, timeout=2.0)
|
||||
await asyncio.sleep(0.1) # Give time for rejections
|
||||
|
||||
@@ -151,7 +151,7 @@ async def test_script_queued(
|
||||
# Test 2: Ring buffer order
|
||||
test_service = next((s for s in services if s.name == "test_ring_buffer"), None)
|
||||
assert test_service is not None, "test_ring_buffer service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test2_complete, timeout=2.0)
|
||||
|
||||
# Verify Test 2
|
||||
@@ -165,7 +165,7 @@ async def test_script_queued(
|
||||
# Test 3: Stop clears queue
|
||||
test_service = next((s for s in services if s.name == "test_stop_clears"), None)
|
||||
assert test_service is not None, "test_stop_clears service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test3_complete, timeout=2.0)
|
||||
|
||||
# Verify Test 3
|
||||
@@ -179,7 +179,7 @@ async def test_script_queued(
|
||||
# Test 4: Rejection enforcement (max_runs=3)
|
||||
test_service = next((s for s in services if s.name == "test_rejection"), None)
|
||||
assert test_service is not None, "test_rejection service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test4_complete, timeout=2.0)
|
||||
await asyncio.sleep(0.1) # Give time for rejections
|
||||
|
||||
@@ -194,7 +194,7 @@ async def test_script_queued(
|
||||
# Test 5: No parameters
|
||||
test_service = next((s for s in services if s.name == "test_no_params"), None)
|
||||
assert test_service is not None, "test_no_params service not found"
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
await asyncio.wait_for(test5_complete, timeout=2.0)
|
||||
|
||||
# Verify Test 5
|
||||
|
||||
@@ -86,7 +86,7 @@ async def test_wait_until_mid_loop_timing(
|
||||
assert test_service is not None, "test_mid_loop_timeout service not found"
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
|
||||
# Wait for test to complete (100ms delay + 200ms timeout + margins = ~500ms)
|
||||
await asyncio.wait_for(test_complete, timeout=5.0)
|
||||
|
||||
@@ -74,7 +74,7 @@ async def test_wait_until_on_boot(
|
||||
)
|
||||
assert set_flag_service is not None, "set_test_flag service not found"
|
||||
|
||||
client.execute_service(set_flag_service, {})
|
||||
await client.execute_service(set_flag_service, {})
|
||||
|
||||
# If the fix works, wait_until's loop() will check the condition and proceed
|
||||
# If the bug exists, wait_until is stuck with disabled loop and will timeout
|
||||
|
||||
@@ -71,7 +71,7 @@ async def test_wait_until_fifo_ordering(
|
||||
assert test_service is not None, "test_wait_until_fifo service not found"
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(test_service, {})
|
||||
await client.execute_service(test_service, {})
|
||||
|
||||
# Wait for test to complete
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user