mirror of
https://github.com/esphome/esphome.git
synced 2025-11-14 13:55:45 +00:00
[api] Store YAML service names in flash instead of heap
Reduces memory usage for YAML-defined API services by storing service names and argument names as pointers to string literals in flash instead of heap-allocated std::string objects. Implementation: - Created UserServiceBase<Ts...> for YAML services (const char* storage) - Created UserServiceDynamic<Ts...> for custom_api_device (std::string storage) - Updated CustomAPIDeviceService to inherit from UserServiceDynamic - UserServiceTrigger uses UserServiceBase (YAML-only) Memory savings per YAML service: - 0 args: 32 bytes (57% reduction) - 2 args: 48 bytes (60% reduction) - 5 args: 96 bytes (63% reduction) Custom API device services maintain same memory footprint (no regression). Typical ESPHome device (2-5 services): 100-240 bytes saved High-service device (10+ services): 400-800 bytes saved 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,11 @@
|
|||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<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,
|
||||||
void (T::*callback)(Ts...))
|
void (T::*callback)(Ts...))
|
||||||
: UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
|||||||
|
|
||||||
template<typename T> enums::ServiceArgType to_service_arg_type();
|
template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||||
|
|
||||||
|
// Base class for YAML-defined services (most common case)
|
||||||
|
// Stores only pointers to string literals in flash - no heap allocation
|
||||||
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||||
public:
|
public:
|
||||||
UserServiceBase(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||||
: name_(std::move(name)), arg_names_(arg_names) {
|
: name_(name), arg_names_(arg_names) {
|
||||||
this->key_ = fnv1_hash(this->name_);
|
this->key_ = fnv1_hash(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||||
@@ -47,7 +49,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
|||||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||||
if (req.key != this->key_)
|
if (req.key != this->key_)
|
||||||
return false;
|
return false;
|
||||||
if (req.args.size() != this->arg_names_.size())
|
if (req.args.size() != sizeof...(Ts))
|
||||||
return false;
|
return false;
|
||||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||||
return true;
|
return true;
|
||||||
@@ -59,14 +61,60 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
|||||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string name_;
|
// Pointers to string literals in flash - no heap allocation
|
||||||
|
const char *name_;
|
||||||
|
std::array<const char *, sizeof...(Ts)> arg_names_;
|
||||||
uint32_t key_{0};
|
uint32_t key_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Derived class for custom_api_device services (rare case)
|
||||||
|
// Stores copies of runtime-generated names
|
||||||
|
template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor {
|
||||||
|
public:
|
||||||
|
UserServiceDynamic(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
||||||
|
: name_(name), arg_names_(arg_names) {
|
||||||
|
this->key_ = fnv1_hash(this->name_.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||||
|
ListEntitiesServicesResponse msg;
|
||||||
|
msg.set_name(StringRef(this->name_));
|
||||||
|
msg.key = this->key_;
|
||||||
|
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||||
|
msg.args.init(sizeof...(Ts));
|
||||||
|
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||||
|
auto &arg = msg.args.emplace_back();
|
||||||
|
arg.type = arg_types[i];
|
||||||
|
arg.set_name(StringRef(this->arg_names_[i]));
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||||
|
if (req.key != this->key_)
|
||||||
|
return false;
|
||||||
|
if (req.args.size() != sizeof...(Ts))
|
||||||
|
return false;
|
||||||
|
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void execute(Ts... x) = 0;
|
||||||
|
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||||
|
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heap-allocated strings for runtime-generated names
|
||||||
|
std::string name_;
|
||||||
std::array<std::string, sizeof...(Ts)> arg_names_;
|
std::array<std::string, sizeof...(Ts)> arg_names_;
|
||||||
|
uint32_t key_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
||||||
public:
|
public:
|
||||||
UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
// Constructor for static names (YAML-defined services - used by code generator)
|
||||||
|
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||||
: UserServiceBase<Ts...>(name, arg_names) {}
|
: UserServiceBase<Ts...>(name, arg_names) {}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
59
tests/integration/fixtures/api_user_services_union.yaml
Normal file
59
tests/integration/fixtures/api_user_services_union.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
esphome:
|
||||||
|
name: test-user-services-union
|
||||||
|
friendly_name: Test User Services Union Storage
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32dev
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "test"
|
||||||
|
password: "password"
|
||||||
|
|
||||||
|
api:
|
||||||
|
actions:
|
||||||
|
# Test service with no arguments
|
||||||
|
- action: test_no_args
|
||||||
|
then:
|
||||||
|
- logger.log: "No args service called"
|
||||||
|
|
||||||
|
# Test service with one argument
|
||||||
|
- action: test_one_arg
|
||||||
|
variables:
|
||||||
|
value: int
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
format: "One arg service: %d"
|
||||||
|
args: [value]
|
||||||
|
|
||||||
|
# Test service with multiple arguments of different types
|
||||||
|
- action: test_multi_args
|
||||||
|
variables:
|
||||||
|
int_val: int
|
||||||
|
float_val: float
|
||||||
|
str_val: string
|
||||||
|
bool_val: bool
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
format: "Multi args: %d, %.2f, %s, %d"
|
||||||
|
args: [int_val, float_val, str_val.c_str(), bool_val]
|
||||||
|
|
||||||
|
# Test service with max typical arguments
|
||||||
|
- action: test_many_args
|
||||||
|
variables:
|
||||||
|
arg1: int
|
||||||
|
arg2: int
|
||||||
|
arg3: int
|
||||||
|
arg4: string
|
||||||
|
arg5: float
|
||||||
|
then:
|
||||||
|
- logger.log: "Many args service called"
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Test Binary Sensor"
|
||||||
|
id: test_sensor
|
||||||
Reference in New Issue
Block a user