mirror of
https://github.com/esphome/esphome.git
synced 2025-07-28 06:06:33 +00:00
This commit is contained in:
parent
0138ef36cf
commit
dd5ba5a90c
@ -24,8 +24,9 @@ from esphome.const import (
|
|||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_VARIABLES,
|
CONF_VARIABLES,
|
||||||
)
|
)
|
||||||
from esphome.core import coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
|
|
||||||
|
DOMAIN = "api"
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
AUTO_LOAD = ["socket"]
|
AUTO_LOAD = ["socket"]
|
||||||
CODEOWNERS = ["@OttoWinter"]
|
CODEOWNERS = ["@OttoWinter"]
|
||||||
@ -51,6 +52,7 @@ SERVICE_ARG_NATIVE_TYPES = {
|
|||||||
}
|
}
|
||||||
CONF_ENCRYPTION = "encryption"
|
CONF_ENCRYPTION = "encryption"
|
||||||
CONF_BATCH_DELAY = "batch_delay"
|
CONF_BATCH_DELAY = "batch_delay"
|
||||||
|
CONF_CUSTOM_SERVICES = "custom_services"
|
||||||
|
|
||||||
|
|
||||||
def validate_encryption_key(value):
|
def validate_encryption_key(value):
|
||||||
@ -115,6 +117,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.positive_time_period_milliseconds,
|
cv.positive_time_period_milliseconds,
|
||||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||||
single=True
|
single=True
|
||||||
),
|
),
|
||||||
@ -139,8 +142,11 @@ async def to_code(config):
|
|||||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||||
|
|
||||||
|
# Set USE_API_SERVICES if any services are enabled
|
||||||
|
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||||
|
cg.add_define("USE_API_SERVICES")
|
||||||
|
|
||||||
if actions := config.get(CONF_ACTIONS, []):
|
if actions := config.get(CONF_ACTIONS, []):
|
||||||
cg.add_define("USE_API_YAML_SERVICES")
|
|
||||||
for conf in actions:
|
for conf in actions:
|
||||||
template_args = []
|
template_args = []
|
||||||
func_args = []
|
func_args = []
|
||||||
@ -317,7 +323,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args):
|
|||||||
|
|
||||||
|
|
||||||
def FILTER_SOURCE_FILES() -> list[str]:
|
def FILTER_SOURCE_FILES() -> list[str]:
|
||||||
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled."""
|
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled
|
||||||
|
and user_services.cpp when no services are defined."""
|
||||||
|
files_to_filter = []
|
||||||
|
|
||||||
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
|
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
|
||||||
# This is a particularly large file that still needs to be opened and read
|
# This is a particularly large file that still needs to be opened and read
|
||||||
# all the way to the end even when ifdef'd out
|
# all the way to the end even when ifdef'd out
|
||||||
@ -325,6 +334,11 @@ def FILTER_SOURCE_FILES() -> list[str]:
|
|||||||
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
|
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
|
||||||
# which happens when the logger level is VERY_VERBOSE
|
# which happens when the logger level is VERY_VERBOSE
|
||||||
if get_logger_level() != "VERY_VERBOSE":
|
if get_logger_level() != "VERY_VERBOSE":
|
||||||
return ["api_pb2_dump.cpp"]
|
files_to_filter.append("api_pb2_dump.cpp")
|
||||||
|
|
||||||
return []
|
# user_services.cpp is only needed when services are defined
|
||||||
|
config = CORE.config.get(DOMAIN, {})
|
||||||
|
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
|
||||||
|
files_to_filter.append("user_services.cpp")
|
||||||
|
|
||||||
|
return files_to_filter
|
||||||
|
@ -807,18 +807,21 @@ enum ServiceArgType {
|
|||||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
|
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
|
||||||
}
|
}
|
||||||
message ListEntitiesServicesArgument {
|
message ListEntitiesServicesArgument {
|
||||||
|
option (ifdef) = "USE_API_SERVICES";
|
||||||
string name = 1;
|
string name = 1;
|
||||||
ServiceArgType type = 2;
|
ServiceArgType type = 2;
|
||||||
}
|
}
|
||||||
message ListEntitiesServicesResponse {
|
message ListEntitiesServicesResponse {
|
||||||
option (id) = 41;
|
option (id) = 41;
|
||||||
option (source) = SOURCE_SERVER;
|
option (source) = SOURCE_SERVER;
|
||||||
|
option (ifdef) = "USE_API_SERVICES";
|
||||||
|
|
||||||
string name = 1;
|
string name = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
repeated ListEntitiesServicesArgument args = 3;
|
repeated ListEntitiesServicesArgument args = 3;
|
||||||
}
|
}
|
||||||
message ExecuteServiceArgument {
|
message ExecuteServiceArgument {
|
||||||
|
option (ifdef) = "USE_API_SERVICES";
|
||||||
bool bool_ = 1;
|
bool bool_ = 1;
|
||||||
int32 legacy_int = 2;
|
int32 legacy_int = 2;
|
||||||
float float_ = 3;
|
float float_ = 3;
|
||||||
@ -834,6 +837,7 @@ message ExecuteServiceRequest {
|
|||||||
option (id) = 42;
|
option (id) = 42;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (ifdef) = "USE_API_SERVICES";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
repeated ExecuteServiceArgument args = 2;
|
repeated ExecuteServiceArgument args = 2;
|
||||||
|
@ -1551,6 +1551,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (auto *service : this->parent_->get_user_services()) {
|
for (auto *service : this->parent_->get_user_services()) {
|
||||||
@ -1562,6 +1563,7 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
|||||||
ESP_LOGV(TAG, "Could not find service");
|
ESP_LOGV(TAG, "Could not find service");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) {
|
NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) {
|
||||||
psk_t psk{};
|
psk_t psk{};
|
||||||
|
@ -195,7 +195,9 @@ class APIConnection : public APIServerConnection {
|
|||||||
// TODO
|
// TODO
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||||
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
|
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
@ -2051,6 +2051,7 @@ void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixe
|
|||||||
void GetTimeResponse::calculate_size(uint32_t &total_size) const {
|
void GetTimeResponse::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0);
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 2: {
|
case 2: {
|
||||||
@ -2245,6 +2246,7 @@ void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0);
|
||||||
ProtoSize::add_repeated_message(total_size, 1, this->args);
|
ProtoSize::add_repeated_message(total_size, 1, this->args);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
|
@ -82,6 +82,7 @@ enum LogLevel : uint32_t {
|
|||||||
LOG_LEVEL_VERBOSE = 6,
|
LOG_LEVEL_VERBOSE = 6,
|
||||||
LOG_LEVEL_VERY_VERBOSE = 7,
|
LOG_LEVEL_VERY_VERBOSE = 7,
|
||||||
};
|
};
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
enum ServiceArgType : uint32_t {
|
enum ServiceArgType : uint32_t {
|
||||||
SERVICE_ARG_TYPE_BOOL = 0,
|
SERVICE_ARG_TYPE_BOOL = 0,
|
||||||
SERVICE_ARG_TYPE_INT = 1,
|
SERVICE_ARG_TYPE_INT = 1,
|
||||||
@ -92,6 +93,7 @@ enum ServiceArgType : uint32_t {
|
|||||||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
|
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
|
||||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7,
|
SERVICE_ARG_TYPE_STRING_ARRAY = 7,
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
enum ClimateMode : uint32_t {
|
enum ClimateMode : uint32_t {
|
||||||
CLIMATE_MODE_OFF = 0,
|
CLIMATE_MODE_OFF = 0,
|
||||||
@ -1203,6 +1205,7 @@ class GetTimeResponse : public ProtoMessage {
|
|||||||
protected:
|
protected:
|
||||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||||
};
|
};
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
class ListEntitiesServicesArgument : public ProtoMessage {
|
class ListEntitiesServicesArgument : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
std::string name{};
|
std::string name{};
|
||||||
@ -1278,6 +1281,7 @@ class ExecuteServiceRequest : public ProtoMessage {
|
|||||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
|
class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
|
@ -162,6 +162,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
|
|||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
|
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case enums::SERVICE_ARG_TYPE_BOOL:
|
case enums::SERVICE_ARG_TYPE_BOOL:
|
||||||
@ -184,6 +185,7 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
|
|||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
|
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
@ -1811,6 +1813,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
|
|||||||
out.append("\n");
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
|
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
|
||||||
__attribute__((unused)) char buffer[64];
|
__attribute__((unused)) char buffer[64];
|
||||||
out.append("ListEntitiesServicesArgument {\n");
|
out.append("ListEntitiesServicesArgument {\n");
|
||||||
@ -1910,6 +1913,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
|
|||||||
}
|
}
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
void ListEntitiesCameraResponse::dump_to(std::string &out) const {
|
void ListEntitiesCameraResponse::dump_to(std::string &out) const {
|
||||||
__attribute__((unused)) char buffer[64];
|
__attribute__((unused)) char buffer[64];
|
||||||
|
@ -195,6 +195,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
this->on_home_assistant_state_response(msg);
|
this->on_home_assistant_state_response(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
case 42: {
|
case 42: {
|
||||||
ExecuteServiceRequest msg;
|
ExecuteServiceRequest msg;
|
||||||
msg.decode(msg_data, msg_size);
|
msg.decode(msg_data, msg_size);
|
||||||
@ -204,6 +205,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
this->on_execute_service_request(msg);
|
this->on_execute_service_request(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
case 45: {
|
case 45: {
|
||||||
CameraImageRequest msg;
|
CameraImageRequest msg;
|
||||||
@ -660,11 +662,13 @@ void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
|
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
if (this->check_authenticated_()) {
|
||||||
this->execute_service(msg);
|
this->execute_service(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||||
if (this->check_authenticated_()) {
|
if (this->check_authenticated_()) {
|
||||||
|
@ -69,7 +69,9 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
virtual void on_get_time_request(const GetTimeRequest &value){};
|
virtual void on_get_time_request(const GetTimeRequest &value){};
|
||||||
virtual void on_get_time_response(const GetTimeResponse &value){};
|
virtual void on_get_time_response(const GetTimeResponse &value){};
|
||||||
|
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
|
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
virtual void on_camera_image_request(const CameraImageRequest &value){};
|
virtual void on_camera_image_request(const CameraImageRequest &value){};
|
||||||
@ -216,7 +218,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
||||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||||
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
|
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||||
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||||
#endif
|
#endif
|
||||||
@ -333,7 +337,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
||||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||||
void on_get_time_request(const GetTimeRequest &msg) override;
|
void on_get_time_request(const GetTimeRequest &msg) override;
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||||
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
@ -24,14 +24,6 @@ static const char *const TAG = "api";
|
|||||||
// APIServer
|
// APIServer
|
||||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
#ifndef USE_API_YAML_SERVICES
|
|
||||||
// Global empty vector to avoid guard variables (saves 8 bytes)
|
|
||||||
// This is initialized at program startup before any threads
|
|
||||||
static const std::vector<UserServiceDescriptor *> empty_user_services{};
|
|
||||||
|
|
||||||
const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance() { return empty_user_services; }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
APIServer::APIServer() {
|
APIServer::APIServer() {
|
||||||
global_api_server = this;
|
global_api_server = this;
|
||||||
// Pre-allocate shared write buffer
|
// Pre-allocate shared write buffer
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "list_entities.h"
|
#include "list_entities.h"
|
||||||
#include "subscribe_state.h"
|
#include "subscribe_state.h"
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
#include "user_services.h"
|
#include "user_services.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@ -25,11 +27,6 @@ struct SavedNoisePsk {
|
|||||||
} PACKED; // NOLINT
|
} PACKED; // NOLINT
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef USE_API_YAML_SERVICES
|
|
||||||
// Forward declaration of helper function
|
|
||||||
const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
class APIServer : public Component, public Controller {
|
class APIServer : public Component, public Controller {
|
||||||
public:
|
public:
|
||||||
APIServer();
|
APIServer();
|
||||||
@ -112,18 +109,9 @@ class APIServer : public Component, public Controller {
|
|||||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||||
#endif
|
#endif
|
||||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||||
void register_user_service(UserServiceDescriptor *descriptor) {
|
#ifdef USE_API_SERVICES
|
||||||
#ifdef USE_API_YAML_SERVICES
|
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||||
// Vector is pre-allocated when services are defined in YAML
|
|
||||||
this->user_services_.push_back(descriptor);
|
|
||||||
#else
|
|
||||||
// Lazy allocate vector on first use for CustomAPIDevice
|
|
||||||
if (!this->user_services_) {
|
|
||||||
this->user_services_ = std::make_unique<std::vector<UserServiceDescriptor *>>();
|
|
||||||
}
|
|
||||||
this->user_services_->push_back(descriptor);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
void request_time();
|
void request_time();
|
||||||
#endif
|
#endif
|
||||||
@ -152,17 +140,9 @@ class APIServer : public Component, public Controller {
|
|||||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||||
std::function<void(std::string)> f);
|
std::function<void(std::string)> f);
|
||||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||||
const std::vector<UserServiceDescriptor *> &get_user_services() const {
|
#ifdef USE_API_SERVICES
|
||||||
#ifdef USE_API_YAML_SERVICES
|
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
||||||
return this->user_services_;
|
|
||||||
#else
|
|
||||||
if (this->user_services_) {
|
|
||||||
return *this->user_services_;
|
|
||||||
}
|
|
||||||
// Return reference to global empty instance (no guard needed)
|
|
||||||
return get_empty_user_services_instance();
|
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||||
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
|
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
|
||||||
@ -194,14 +174,8 @@ class APIServer : public Component, public Controller {
|
|||||||
#endif
|
#endif
|
||||||
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
||||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||||
#ifdef USE_API_YAML_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
// When services are defined in YAML, we know at compile time that services will be registered
|
|
||||||
std::vector<UserServiceDescriptor *> user_services_;
|
std::vector<UserServiceDescriptor *> user_services_;
|
||||||
#else
|
|
||||||
// Services can still be registered at runtime by CustomAPIDevice components even when not
|
|
||||||
// defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common
|
|
||||||
// case where no services (YAML or custom) are used.
|
|
||||||
std::unique_ptr<std::vector<UserServiceDescriptor *>> user_services_;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Group smaller types together
|
// Group smaller types together
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include "api_server.h"
|
#include "api_server.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
#include "user_services.h"
|
#include "user_services.h"
|
||||||
|
#endif
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
||||||
public:
|
public:
|
||||||
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
|
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
|
||||||
@ -19,6 +22,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
|
|||||||
T *obj_;
|
T *obj_;
|
||||||
void (T::*callback_)(Ts...);
|
void (T::*callback_)(Ts...);
|
||||||
};
|
};
|
||||||
|
#endif // USE_API_SERVICES
|
||||||
|
|
||||||
class CustomAPIDevice {
|
class CustomAPIDevice {
|
||||||
public:
|
public:
|
||||||
@ -46,12 +50,14 @@ class CustomAPIDevice {
|
|||||||
* @param name The name of the service to register.
|
* @param name The name of the service to register.
|
||||||
* @param arg_names The name of the arguments for the service, must match the arguments of the function.
|
* @param arg_names The name of the arguments for the service, must match the arguments of the function.
|
||||||
*/
|
*/
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
template<typename T, typename... Ts>
|
template<typename T, typename... Ts>
|
||||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||||
global_api_server->register_user_service(service);
|
global_api_server->register_user_service(service);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/** Register a custom native API service that will show up in Home Assistant.
|
/** Register a custom native API service that will show up in Home Assistant.
|
||||||
*
|
*
|
||||||
@ -71,10 +77,12 @@ class CustomAPIDevice {
|
|||||||
* @param callback The member function to call when the service is triggered.
|
* @param callback The member function to call when the service is triggered.
|
||||||
* @param name The name of the arguments for the service, must match the arguments of the function.
|
* @param name The name of the arguments for the service, must match the arguments of the function.
|
||||||
*/
|
*/
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||||
global_api_server->register_user_service(service);
|
global_api_server->register_user_service(service);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
||||||
*
|
*
|
||||||
|
@ -83,10 +83,12 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
|
|||||||
|
|
||||||
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
|
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
|
||||||
|
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||||
auto resp = service->encode_list_service_response();
|
auto resp = service->encode_list_service_response();
|
||||||
return this->client_->send_message(resp);
|
return this->client_->send_message(resp);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
} // namespace api
|
} // namespace api
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -44,7 +44,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
|||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
bool on_service(UserServiceDescriptor *service) override;
|
bool on_service(UserServiceDescriptor *service) override;
|
||||||
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
bool on_camera(camera::Camera *entity) override;
|
bool on_camera(camera::Camera *entity) override;
|
||||||
#endif
|
#endif
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
|
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
@ -73,3 +74,4 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...
|
|||||||
|
|
||||||
} // namespace api
|
} // namespace api
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
#endif // USE_API_SERVICES
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
#include "esphome/components/api/api_server.h"
|
#include "esphome/components/api/api_server.h"
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
#include "esphome/components/api/user_services.h"
|
#include "esphome/components/api/user_services.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -148,7 +150,7 @@ void ComponentIterator::advance() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API
|
#ifdef USE_API_SERVICES
|
||||||
case IteratorState ::SERVICE:
|
case IteratorState ::SERVICE:
|
||||||
if (this->at_ >= api::global_api_server->get_user_services().size()) {
|
if (this->at_ >= api::global_api_server->get_user_services().size()) {
|
||||||
advance_platform = true;
|
advance_platform = true;
|
||||||
@ -383,7 +385,7 @@ void ComponentIterator::advance() {
|
|||||||
}
|
}
|
||||||
bool ComponentIterator::on_end() { return true; }
|
bool ComponentIterator::on_end() { return true; }
|
||||||
bool ComponentIterator::on_begin() { return true; }
|
bool ComponentIterator::on_begin() { return true; }
|
||||||
#ifdef USE_API
|
#ifdef USE_API_SERVICES
|
||||||
bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; }
|
bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; }
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
#ifdef USE_API
|
#ifdef USE_API_SERVICES
|
||||||
namespace api {
|
namespace api {
|
||||||
class UserServiceDescriptor;
|
class UserServiceDescriptor;
|
||||||
} // namespace api
|
} // namespace api
|
||||||
@ -45,7 +45,7 @@ class ComponentIterator {
|
|||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0;
|
virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API
|
#ifdef USE_API_SERVICES
|
||||||
virtual bool on_service(api::UserServiceDescriptor *service);
|
virtual bool on_service(api::UserServiceDescriptor *service);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
@ -122,7 +122,7 @@ class ComponentIterator {
|
|||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
TEXT_SENSOR,
|
TEXT_SENSOR,
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API
|
#ifdef USE_API_SERVICES
|
||||||
SERVICE,
|
SERVICE,
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
|
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||||
#define USE_API_NOISE
|
#define USE_API_NOISE
|
||||||
#define USE_API_PLAINTEXT
|
#define USE_API_PLAINTEXT
|
||||||
#define USE_API_YAML_SERVICES
|
#define USE_API_SERVICES
|
||||||
#define USE_MD5
|
#define USE_MD5
|
||||||
#define USE_MQTT
|
#define USE_MQTT
|
||||||
#define USE_NETWORK
|
#define USE_NETWORK
|
||||||
|
24
tests/integration/fixtures/api_custom_services.yaml
Normal file
24
tests/integration/fixtures/api_custom_services.yaml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
esphome:
|
||||||
|
name: api-custom-services-test
|
||||||
|
host:
|
||||||
|
|
||||||
|
# This is required for CustomAPIDevice to work
|
||||||
|
api:
|
||||||
|
custom_services: true
|
||||||
|
# Also test that YAML services still work
|
||||||
|
actions:
|
||||||
|
- action: test_yaml_service
|
||||||
|
then:
|
||||||
|
- logger.log: "YAML service called"
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
# External component that uses CustomAPIDevice
|
||||||
|
external_components:
|
||||||
|
- source:
|
||||||
|
type: local
|
||||||
|
path: EXTERNAL_COMPONENT_PATH
|
||||||
|
components: [custom_api_device_component]
|
||||||
|
|
||||||
|
custom_api_device_component:
|
@ -0,0 +1,19 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
custom_api_device_component_ns = cg.esphome_ns.namespace("custom_api_device_component")
|
||||||
|
CustomAPIDeviceComponent = custom_api_device_component_ns.class_(
|
||||||
|
"CustomAPIDeviceComponent", cg.Component
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(CustomAPIDeviceComponent),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
@ -0,0 +1,53 @@
|
|||||||
|
#include "custom_api_device_component.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef USE_API
|
||||||
|
namespace esphome {
|
||||||
|
namespace custom_api_device_component {
|
||||||
|
|
||||||
|
static const char *const TAG = "custom_api";
|
||||||
|
|
||||||
|
void CustomAPIDeviceComponent::setup() {
|
||||||
|
// Register services using CustomAPIDevice
|
||||||
|
register_service(&CustomAPIDeviceComponent::on_test_service, "custom_test_service");
|
||||||
|
|
||||||
|
register_service(&CustomAPIDeviceComponent::on_service_with_args, "custom_service_with_args",
|
||||||
|
{"arg_string", "arg_int", "arg_bool", "arg_float"});
|
||||||
|
|
||||||
|
// Test array types
|
||||||
|
register_service(&CustomAPIDeviceComponent::on_service_with_arrays, "custom_service_with_arrays",
|
||||||
|
{"bool_array", "int_array", "float_array", "string_array"});
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomAPIDeviceComponent::on_test_service() { ESP_LOGI(TAG, "Custom test service called!"); }
|
||||||
|
|
||||||
|
// NOLINTNEXTLINE(performance-unnecessary-value-param)
|
||||||
|
void CustomAPIDeviceComponent::on_service_with_args(std::string arg_string, int32_t arg_int, bool arg_bool,
|
||||||
|
float arg_float) {
|
||||||
|
ESP_LOGI(TAG, "Custom service called with: %s, %d, %d, %.2f", arg_string.c_str(), arg_int, arg_bool, arg_float);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomAPIDeviceComponent::on_service_with_arrays(std::vector<bool> bool_array, std::vector<int32_t> int_array,
|
||||||
|
std::vector<float> float_array,
|
||||||
|
std::vector<std::string> string_array) {
|
||||||
|
ESP_LOGI(TAG, "Array service called with %zu bools, %zu ints, %zu floats, %zu strings", bool_array.size(),
|
||||||
|
int_array.size(), float_array.size(), string_array.size());
|
||||||
|
|
||||||
|
// Log first element of each array if not empty
|
||||||
|
if (!bool_array.empty()) {
|
||||||
|
ESP_LOGI(TAG, "First bool: %s", bool_array[0] ? "true" : "false");
|
||||||
|
}
|
||||||
|
if (!int_array.empty()) {
|
||||||
|
ESP_LOGI(TAG, "First int: %d", int_array[0]);
|
||||||
|
}
|
||||||
|
if (!float_array.empty()) {
|
||||||
|
ESP_LOGI(TAG, "First float: %.2f", float_array[0]);
|
||||||
|
}
|
||||||
|
if (!string_array.empty()) {
|
||||||
|
ESP_LOGI(TAG, "First string: %s", string_array[0].c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace custom_api_device_component
|
||||||
|
} // namespace esphome
|
||||||
|
#endif // USE_API
|
@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/api/custom_api_device.h"
|
||||||
|
|
||||||
|
#ifdef USE_API
|
||||||
|
namespace esphome {
|
||||||
|
namespace custom_api_device_component {
|
||||||
|
|
||||||
|
using namespace api;
|
||||||
|
|
||||||
|
class CustomAPIDeviceComponent : public Component, public CustomAPIDevice {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
void on_test_service();
|
||||||
|
|
||||||
|
// NOLINTNEXTLINE(performance-unnecessary-value-param)
|
||||||
|
void on_service_with_args(std::string arg_string, int32_t arg_int, bool arg_bool, float arg_float);
|
||||||
|
|
||||||
|
void on_service_with_arrays(std::vector<bool> bool_array, std::vector<int32_t> int_array,
|
||||||
|
std::vector<float> float_array, std::vector<std::string> string_array);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace custom_api_device_component
|
||||||
|
} // namespace esphome
|
||||||
|
#endif // USE_API
|
144
tests/integration/test_api_custom_services.py
Normal file
144
tests/integration/test_api_custom_services.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
"""Integration test for API custom services using CustomAPIDevice."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
from aioesphomeapi import UserService, UserServiceArgType
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_custom_services(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test CustomAPIDevice services work correctly with custom_services: true."""
|
||||||
|
# Get the path to the external components directory
|
||||||
|
external_components_path = str(
|
||||||
|
Path(__file__).parent / "fixtures" / "external_components"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace the placeholder in the YAML config with the actual path
|
||||||
|
yaml_config = yaml_config.replace(
|
||||||
|
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||||
|
)
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
# Track log messages
|
||||||
|
yaml_service_future = loop.create_future()
|
||||||
|
custom_service_future = loop.create_future()
|
||||||
|
custom_args_future = loop.create_future()
|
||||||
|
custom_arrays_future = loop.create_future()
|
||||||
|
|
||||||
|
# Patterns to match in logs
|
||||||
|
yaml_service_pattern = re.compile(r"YAML service called")
|
||||||
|
custom_service_pattern = re.compile(r"Custom test service called!")
|
||||||
|
custom_args_pattern = re.compile(
|
||||||
|
r"Custom service called with: test_string, 456, 1, 78\.90"
|
||||||
|
)
|
||||||
|
custom_arrays_pattern = re.compile(
|
||||||
|
r"Array service called with 2 bools, 3 ints, 2 floats, 2 strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_output(line: str) -> None:
|
||||||
|
"""Check log output for expected messages."""
|
||||||
|
if not yaml_service_future.done() and yaml_service_pattern.search(line):
|
||||||
|
yaml_service_future.set_result(True)
|
||||||
|
elif not custom_service_future.done() and custom_service_pattern.search(line):
|
||||||
|
custom_service_future.set_result(True)
|
||||||
|
elif not custom_args_future.done() and custom_args_pattern.search(line):
|
||||||
|
custom_args_future.set_result(True)
|
||||||
|
elif not custom_arrays_future.done() and custom_arrays_pattern.search(line):
|
||||||
|
custom_arrays_future.set_result(True)
|
||||||
|
|
||||||
|
# Run with log monitoring
|
||||||
|
async with run_compiled(yaml_config, line_callback=check_output):
|
||||||
|
async with 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-custom-services-test"
|
||||||
|
|
||||||
|
# List services
|
||||||
|
_, services = await client.list_entities_services()
|
||||||
|
|
||||||
|
# Should have 4 services: 1 YAML + 3 CustomAPIDevice
|
||||||
|
assert len(services) == 4, f"Expected 4 services, found {len(services)}"
|
||||||
|
|
||||||
|
# Find our services
|
||||||
|
yaml_service: UserService | None = None
|
||||||
|
custom_service: UserService | None = None
|
||||||
|
custom_args_service: UserService | None = None
|
||||||
|
custom_arrays_service: UserService | None = None
|
||||||
|
|
||||||
|
for service in services:
|
||||||
|
if service.name == "test_yaml_service":
|
||||||
|
yaml_service = service
|
||||||
|
elif service.name == "custom_test_service":
|
||||||
|
custom_service = service
|
||||||
|
elif service.name == "custom_service_with_args":
|
||||||
|
custom_args_service = service
|
||||||
|
elif service.name == "custom_service_with_arrays":
|
||||||
|
custom_arrays_service = service
|
||||||
|
|
||||||
|
assert yaml_service is not None, "test_yaml_service not found"
|
||||||
|
assert custom_service is not None, "custom_test_service not found"
|
||||||
|
assert custom_args_service is not None, "custom_service_with_args not found"
|
||||||
|
assert custom_arrays_service is not None, (
|
||||||
|
"custom_service_with_arrays not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test YAML service
|
||||||
|
client.execute_service(yaml_service, {})
|
||||||
|
await asyncio.wait_for(yaml_service_future, timeout=5.0)
|
||||||
|
|
||||||
|
# Test simple CustomAPIDevice service
|
||||||
|
client.execute_service(custom_service, {})
|
||||||
|
await asyncio.wait_for(custom_service_future, timeout=5.0)
|
||||||
|
|
||||||
|
# Verify custom_args_service arguments
|
||||||
|
assert len(custom_args_service.args) == 4
|
||||||
|
arg_types = {arg.name: arg.type for arg in custom_args_service.args}
|
||||||
|
assert arg_types["arg_string"] == UserServiceArgType.STRING
|
||||||
|
assert arg_types["arg_int"] == UserServiceArgType.INT
|
||||||
|
assert arg_types["arg_bool"] == UserServiceArgType.BOOL
|
||||||
|
assert arg_types["arg_float"] == UserServiceArgType.FLOAT
|
||||||
|
|
||||||
|
# Test CustomAPIDevice service with arguments
|
||||||
|
client.execute_service(
|
||||||
|
custom_args_service,
|
||||||
|
{
|
||||||
|
"arg_string": "test_string",
|
||||||
|
"arg_int": 456,
|
||||||
|
"arg_bool": True,
|
||||||
|
"arg_float": 78.9,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await asyncio.wait_for(custom_args_future, timeout=5.0)
|
||||||
|
|
||||||
|
# Verify array service arguments
|
||||||
|
assert len(custom_arrays_service.args) == 4
|
||||||
|
array_arg_types = {arg.name: arg.type for arg in custom_arrays_service.args}
|
||||||
|
assert array_arg_types["bool_array"] == UserServiceArgType.BOOL_ARRAY
|
||||||
|
assert array_arg_types["int_array"] == UserServiceArgType.INT_ARRAY
|
||||||
|
assert array_arg_types["float_array"] == UserServiceArgType.FLOAT_ARRAY
|
||||||
|
assert array_arg_types["string_array"] == UserServiceArgType.STRING_ARRAY
|
||||||
|
|
||||||
|
# Test CustomAPIDevice service with arrays
|
||||||
|
client.execute_service(
|
||||||
|
custom_arrays_service,
|
||||||
|
{
|
||||||
|
"bool_array": [True, False],
|
||||||
|
"int_array": [1, 2, 3],
|
||||||
|
"float_array": [1.1, 2.2],
|
||||||
|
"string_array": ["hello", "world"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await asyncio.wait_for(custom_arrays_future, timeout=5.0)
|
Loading…
x
Reference in New Issue
Block a user