mirror of
https://github.com/esphome/esphome.git
synced 2025-07-27 21:56:34 +00:00
This commit is contained in:
parent
0138ef36cf
commit
dd5ba5a90c
@ -24,8 +24,9 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
DOMAIN = "api"
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
@ -51,6 +52,7 @@ SERVICE_ARG_NATIVE_TYPES = {
|
||||
}
|
||||
CONF_ENCRYPTION = "encryption"
|
||||
CONF_BATCH_DELAY = "batch_delay"
|
||||
CONF_CUSTOM_SERVICES = "custom_services"
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
@ -115,6 +117,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
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(
|
||||
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_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, []):
|
||||
cg.add_define("USE_API_YAML_SERVICES")
|
||||
for conf in actions:
|
||||
template_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]:
|
||||
"""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
|
||||
# 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
|
||||
@ -325,6 +334,11 @@ def FILTER_SOURCE_FILES() -> list[str]:
|
||||
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
|
||||
# which happens when the logger level is 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;
|
||||
}
|
||||
message ListEntitiesServicesArgument {
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
string name = 1;
|
||||
ServiceArgType type = 2;
|
||||
}
|
||||
message ListEntitiesServicesResponse {
|
||||
option (id) = 41;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
|
||||
string name = 1;
|
||||
fixed32 key = 2;
|
||||
repeated ListEntitiesServicesArgument args = 3;
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
bool bool_ = 1;
|
||||
int32 legacy_int = 2;
|
||||
float float_ = 3;
|
||||
@ -834,6 +837,7 @@ message ExecuteServiceRequest {
|
||||
option (id) = 42;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
|
||||
fixed32 key = 1;
|
||||
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) {
|
||||
bool found = false;
|
||||
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");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
psk_t psk{};
|
||||
|
@ -195,7 +195,9 @@ class APIConnection : public APIServerConnection {
|
||||
// TODO
|
||||
return {};
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
|
@ -2051,6 +2051,7 @@ void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixe
|
||||
void GetTimeResponse::calculate_size(uint32_t &total_size) const {
|
||||
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) {
|
||||
switch (field_id) {
|
||||
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_repeated_message(total_size, 1, this->args);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
|
@ -82,6 +82,7 @@ enum LogLevel : uint32_t {
|
||||
LOG_LEVEL_VERBOSE = 6,
|
||||
LOG_LEVEL_VERY_VERBOSE = 7,
|
||||
};
|
||||
#ifdef USE_API_SERVICES
|
||||
enum ServiceArgType : uint32_t {
|
||||
SERVICE_ARG_TYPE_BOOL = 0,
|
||||
SERVICE_ARG_TYPE_INT = 1,
|
||||
@ -92,6 +93,7 @@ enum ServiceArgType : uint32_t {
|
||||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
|
||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
enum ClimateMode : uint32_t {
|
||||
CLIMATE_MODE_OFF = 0,
|
||||
@ -1203,6 +1205,7 @@ class GetTimeResponse : public ProtoMessage {
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
};
|
||||
#ifdef USE_API_SERVICES
|
||||
class ListEntitiesServicesArgument : public ProtoMessage {
|
||||
public:
|
||||
std::string name{};
|
||||
@ -1278,6 +1281,7 @@ class ExecuteServiceRequest : public ProtoMessage {
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
|
||||
public:
|
||||
|
@ -162,6 +162,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
|
||||
switch (value) {
|
||||
case enums::SERVICE_ARG_TYPE_BOOL:
|
||||
@ -184,6 +185,7 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
|
||||
switch (value) {
|
||||
@ -1811,6 +1813,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("ListEntitiesServicesArgument {\n");
|
||||
@ -1910,6 +1913,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
|
||||
}
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void ListEntitiesCameraResponse::dump_to(std::string &out) const {
|
||||
__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);
|
||||
break;
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
case 42: {
|
||||
ExecuteServiceRequest msg;
|
||||
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);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
case 45: {
|
||||
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) {
|
||||
if (this->check_authenticated_()) {
|
||||
this->execute_service(msg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
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_response(const GetTimeResponse &value){};
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
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_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
|
||||
#ifdef USE_API_SERVICES
|
||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||
#endif
|
||||
@ -333,7 +337,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
void on_get_time_request(const GetTimeRequest &msg) override;
|
||||
#ifdef USE_API_SERVICES
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
|
@ -24,14 +24,6 @@ static const char *const TAG = "api";
|
||||
// APIServer
|
||||
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() {
|
||||
global_api_server = this;
|
||||
// Pre-allocate shared write buffer
|
||||
|
@ -12,7 +12,9 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "list_entities.h"
|
||||
#include "subscribe_state.h"
|
||||
#ifdef USE_API_SERVICES
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
|
||||
#include <vector>
|
||||
|
||||
@ -25,11 +27,6 @@ struct SavedNoisePsk {
|
||||
} PACKED; // NOLINT
|
||||
#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 {
|
||||
public:
|
||||
APIServer();
|
||||
@ -112,18 +109,9 @@ class APIServer : public Component, public Controller {
|
||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||
#endif
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||
void register_user_service(UserServiceDescriptor *descriptor) {
|
||||
#ifdef USE_API_YAML_SERVICES
|
||||
// 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);
|
||||
#ifdef USE_API_SERVICES
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void request_time();
|
||||
#endif
|
||||
@ -152,17 +140,9 @@ class APIServer : public Component, public Controller {
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f);
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
const std::vector<UserServiceDescriptor *> &get_user_services() const {
|
||||
#ifdef USE_API_YAML_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();
|
||||
#ifdef USE_API_SERVICES
|
||||
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_API_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
|
||||
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||
#ifdef USE_API_YAML_SERVICES
|
||||
// When services are defined in YAML, we know at compile time that services will be registered
|
||||
#ifdef USE_API_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
|
||||
|
||||
// Group smaller types together
|
||||
|
@ -3,10 +3,13 @@
|
||||
#include <map>
|
||||
#include "api_server.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
||||
public:
|
||||
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_;
|
||||
void (T::*callback_)(Ts...);
|
||||
};
|
||||
#endif // USE_API_SERVICES
|
||||
|
||||
class CustomAPIDevice {
|
||||
public:
|
||||
@ -46,12 +50,14 @@ class CustomAPIDevice {
|
||||
* @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.
|
||||
*/
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T, typename... Ts>
|
||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#endif
|
||||
|
||||
/** 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 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) {
|
||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#endif
|
||||
|
||||
/** 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) {}
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
return this->client_->send_message(resp);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
@ -44,7 +44,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
bool on_service(UserServiceDescriptor *service) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
bool on_camera(camera::Camera *entity) override;
|
||||
#endif
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "api_pb2.h"
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
@ -73,3 +74,4 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
#endif // USE_API_SERVICES
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#include "esphome/components/api/user_services.h"
|
||||
#endif
|
||||
|
||||
@ -148,7 +150,7 @@ void ComponentIterator::advance() {
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
case IteratorState ::SERVICE:
|
||||
if (this->at_ >= api::global_api_server->get_user_services().size()) {
|
||||
advance_platform = true;
|
||||
@ -383,7 +385,7 @@ void ComponentIterator::advance() {
|
||||
}
|
||||
bool ComponentIterator::on_end() { return true; }
|
||||
bool ComponentIterator::on_begin() { return true; }
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; }
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
namespace api {
|
||||
class UserServiceDescriptor;
|
||||
} // namespace api
|
||||
@ -45,7 +45,7 @@ class ComponentIterator {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0;
|
||||
#endif
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
virtual bool on_service(api::UserServiceDescriptor *service);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
@ -122,7 +122,7 @@ class ComponentIterator {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
TEXT_SENSOR,
|
||||
#endif
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
SERVICE,
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
|
@ -108,7 +108,7 @@
|
||||
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
#define USE_API_NOISE
|
||||
#define USE_API_PLAINTEXT
|
||||
#define USE_API_YAML_SERVICES
|
||||
#define USE_API_SERVICES
|
||||
#define USE_MD5
|
||||
#define USE_MQTT
|
||||
#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