1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-15 16:23:48 +01:00

Handle action status response without json

This commit is contained in:
Jesse Hills
2025-10-07 17:25:35 +13:00
parent e95be061b3
commit a11bef0558
11 changed files with 164 additions and 58 deletions

View File

@@ -9,6 +9,7 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION,
CONF_ACTIONS,
CONF_CAPTURE_RESPONSE,
CONF_DATA,
CONF_DATA_TEMPLATE,
CONF_EVENT,
@@ -18,7 +19,7 @@ from esphome.const import (
CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED,
CONF_ON_ERROR,
CONF_ON_RESPONSE,
CONF_ON_SUCCESS,
CONF_PASSWORD,
CONF_PORT,
CONF_REBOOT_TIMEOUT,
@@ -37,9 +38,21 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "api"
DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket", "json"]
CODEOWNERS = ["@esphome/core"]
def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Conditionally auto-load json only when capture_response is used."""
base = ["socket"]
# Check if any homeassistant.action/homeassistant.service has capture_response: true
# This flag is set during config validation in _validate_response_config
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
return base + ["json"]
return base
api_ns = cg.esphome_ns.namespace("api")
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
HomeAssistantServiceCallAction = api_ns.class_(
@@ -296,11 +309,26 @@ async def to_code(config):
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
def _validate_response_config(config):
if CONF_RESPONSE_TEMPLATE in config and not config.get(CONF_ON_RESPONSE):
def _validate_response_config(config: ConfigType) -> ConfigType:
# Validate dependencies:
# - response_template requires capture_response: true
# - capture_response: true requires on_success
if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
raise cv.Invalid(
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_ON_RESPONSE}` to be set."
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
path=[CONF_RESPONSE_TEMPLATE],
)
if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
raise cv.Invalid(
f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
path=[CONF_CAPTURE_RESPONSE],
)
# Track if any action uses capture_response for AUTO_LOAD
if config[CONF_CAPTURE_RESPONSE]:
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
return config
@@ -320,7 +348,8 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
{cv.string: cv.returning_lambda}
),
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True),
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
}
),
@@ -361,29 +390,39 @@ async def homeassistant_service_to_code(
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
if response_template := config.get(CONF_RESPONSE_TEMPLATE):
templ = await cg.templatable(response_template, args, cg.std_string)
cg.add(var.set_response_template(templ))
if on_response := config.get(CONF_ON_RESPONSE):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add(var.set_wants_response())
await automation.build_automation(
var.get_response_trigger(),
[(cg.JsonObject, "response"), *args],
on_response,
)
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
cg.add(var.set_wants_response())
cg.add(var.set_wants_status())
await automation.build_automation(
var.get_error_trigger(),
[(cg.std_string, "error"), *args],
on_error,
)
if on_success := config.get(CONF_ON_SUCCESS):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add(var.set_wants_status())
if config[CONF_CAPTURE_RESPONSE]:
cg.add(var.set_wants_response())
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
await automation.build_automation(
var.get_success_trigger_with_response(),
[(cg.JsonObject, "response"), *args],
on_success,
)
if response_template := config.get(CONF_RESPONSE_TEMPLATE):
templ = await cg.templatable(response_template, args, cg.std_string)
cg.add(var.set_response_template(templ))
else:
await automation.build_automation(
var.get_success_trigger(),
args,
on_success,
)
return var

View File

@@ -780,8 +780,9 @@ message HomeassistantActionRequest {
repeated HomeassistantServiceMap data_template = 3;
repeated HomeassistantServiceMap variables = 4;
bool is_event = 5;
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; // Call ID for response tracking
string response_template = 7 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; // Optional Jinja template for response processing
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
}
// Message sent by Home Assistant to ESPHome with service call response data
@@ -794,7 +795,7 @@ message HomeassistantActionResponse {
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
bool success = 2; // Whether the service call succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(pointer_to_buffer) = true]; // Service response data
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
}
// ==================== IMPORT HOME ASSISTANT STATES ====================

View File

@@ -1552,8 +1552,15 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
msg.response_data_len);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (msg.response_data_len > 0) {
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
msg.response_data_len);
} else
#endif
{
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
}
};
#endif
#ifdef USE_API_NOISE

View File

@@ -887,8 +887,11 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
buffer.encode_uint32(6, this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
buffer.encode_string(7, this->response_template);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
buffer.encode_bool(7, this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
buffer.encode_string(8, this->response_template);
#endif
}
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
@@ -900,7 +903,10 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
size.add_uint32(1, this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
size.add_bool(1, this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
size.add_length(1, this->response_template.size());
#endif
}
@@ -924,12 +930,14 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe
case 3:
this->error_message = value.as_string();
break;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
case 4: {
// Use raw data directly to avoid allocation
this->response_data = value.data();
this->response_data_len = value.size();
break;
}
#endif
default:
return false;
}

View File

@@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
class HomeassistantActionRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 35;
static constexpr uint8_t ESTIMATED_SIZE = 126;
static constexpr uint8_t ESTIMATED_SIZE = 128;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_request"; }
#endif
@@ -1117,7 +1117,10 @@ class HomeassistantActionRequest final : public ProtoMessage {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
uint32_t call_id{0};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
bool wants_response{false};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
std::string response_template{};
#endif
void encode(ProtoWriteBuffer buffer) const override;
@@ -1140,8 +1143,10 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
uint32_t call_id{0};
bool success{false};
std::string error_message{};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif

View File

@@ -1125,7 +1125,10 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
dump_field(out, "call_id", this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
dump_field(out, "wants_response", this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
dump_field(out, "response_template", this->response_template);
#endif
}
@@ -1136,9 +1139,11 @@ void HomeassistantActionResponse::dump_to(std::string &out) const {
dump_field(out, "call_id", this->call_id);
dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message);
#ifdef USE_API_HOMEASSISTANT_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_API_HOMEASSISTANT_STATES

View File

@@ -409,6 +409,16 @@ void APIServer::register_action_response_callback(uint32_t call_id, ActionRespon
this->action_response_callbacks_[call_id] = std::move(callback);
}
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
auto it = this->action_response_callbacks_.find(call_id);
if (it != this->action_response_callbacks_.end()) {
auto callback = std::move(it->second);
this->action_response_callbacks_.erase(it);
auto response = std::make_shared<ActionResponse>(success, error_message);
callback(response);
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len) {
auto it = this->action_response_callbacks_.find(call_id);
@@ -419,6 +429,7 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
callback(response);
}
}
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES

View File

@@ -116,8 +116,11 @@ class APIServer : public Component, public Controller {
// Action response handling
using ActionResponseCallback = std::function<void(std::shared_ptr<class ActionResponse>)>;
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void handle_action_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_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_SERVICES

View File

@@ -51,24 +51,34 @@ template<typename... Ts> class TemplatableKeyValuePair {
// Represents the response data from a Home Assistant action
class ActionResponse {
public:
ActionResponse(bool success, std::string error_message = "", const uint8_t *data = nullptr, size_t data_len = 0)
ActionResponse(bool success, std::string error_message = "")
: success_(success), error_message_(std::move(error_message)) {}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(std::move(error_message)) {
if (data == nullptr || data_len == 0)
return;
this->json_document_ = json::parse_json(data, data_len);
this->json_ = this->json_document_.as<JsonObject>();
}
#endif
bool is_success() const { return this->success_; }
const std::string &get_error_message() const { return this->error_message_; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// Get data as parsed JSON object
JsonObject get_json() { return this->json_; }
#endif
protected:
bool success_;
std::string error_message_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
JsonDocument json_document_;
JsonObject json_;
#endif
};
// Callback type for action responses
@@ -77,7 +87,9 @@ template<typename... Ts> using ActionResponseCallback = std::function<void(std::
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
public:
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) {
this->flags_.is_event = is_event;
}
template<typename T> void set_service(T service) { this->service_ = service; }
@@ -95,22 +107,24 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
this->has_response_template_ = true;
this->flags_.has_response_template = true;
}
void set_wants_response() { this->wants_response_ = true; }
void set_wants_status() { this->flags_.wants_status = true; }
void set_wants_response() { this->flags_.wants_response = true; }
Trigger<JsonObject, Ts...> *get_response_trigger() const { return this->response_trigger_; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<JsonObject, Ts...> *get_success_trigger_with_response() const { return this->success_trigger_with_response_; }
#endif
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(Ts... x) override {
HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value));
resp.is_event = this->is_event_;
resp.is_event = this->flags_.is_event;
for (auto &it : this->data_) {
resp.data.emplace_back();
auto &kv = resp.data.back();
@@ -131,15 +145,18 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
if (this->wants_response_) {
if (this->flags_.wants_status) {
// Generate a unique call ID for this service call
static uint32_t call_id_counter = 1;
uint32_t call_id = call_id_counter++;
resp.call_id = call_id;
// Set response template if provided
if (this->has_response_template_) {
std::string response_template_value = this->response_template_.value(x...);
resp.response_template = response_template_value;
if (this->flags_.wants_response) {
resp.wants_response = true;
// Set response template if provided
if (this->flags_.has_response_template) {
std::string response_template_value = this->response_template_.value(x...);
resp.response_template = response_template_value;
}
}
auto captured_args = std::make_tuple(x...);
@@ -148,15 +165,17 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
std::apply(
[this, &response](auto &&...args) {
if (response->is_success()) {
if (this->response_trigger_ != nullptr) {
this->response_trigger_->trigger(response->get_json(), args...);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (this->flags_.wants_response) {
this->success_trigger_with_response_->trigger(response->get_json(), args...);
} else
#endif
{
this->success_trigger_->trigger(args...);
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS
else if (this->error_trigger_ != nullptr) {
} else {
this->error_trigger_->trigger(response->get_error_message(), args...);
}
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS
},
captured_args);
});
@@ -168,20 +187,26 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
protected:
APIServer *parent_;
bool is_event_;
TemplatableStringValue<Ts...> service_{};
std::vector<TemplatableKeyValuePair<Ts...>> data_;
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
TemplatableStringValue<Ts...> response_template_{""};
Trigger<JsonObject, Ts...> *response_trigger_ = new Trigger<JsonObject, Ts...>();
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS
Trigger<JsonObject, Ts...> *success_trigger_with_response_ = new Trigger<JsonObject, Ts...>();
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
#endif
bool wants_response_{false};
bool has_response_template_{false};
#endif
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct Flags {
uint8_t is_event : 1;
uint8_t wants_status : 1;
uint8_t wants_response : 1;
uint8_t has_response_template : 1;
uint8_t reserved : 5;
} flags_{0};
};
} // namespace esphome::api

View File

@@ -174,6 +174,7 @@ CONF_CALIBRATE_LINEAR = "calibrate_linear"
CONF_CALIBRATION = "calibration"
CONF_CAPACITANCE = "capacitance"
CONF_CAPACITY = "capacity"
CONF_CAPTURE_RESPONSE = "capture_response"
CONF_CARBON_MONOXIDE = "carbon_monoxide"
CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent"
CONF_CARRIER_FREQUENCY = "carrier_frequency"
@@ -675,6 +676,7 @@ CONF_ON_RELEASE = "on_release"
CONF_ON_RESPONSE = "on_response"
CONF_ON_SHUTDOWN = "on_shutdown"
CONF_ON_SPEED_SET = "on_speed_set"
CONF_ON_SUCCESS = "on_success"
CONF_ON_STATE = "on_state"
CONF_ON_TAG = "on_tag"
CONF_ON_TAG_REMOVED = "on_tag_removed"

View File

@@ -113,7 +113,7 @@
#define USE_API_CLIENT_CONNECTED_TRIGGER
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#define USE_API_HOMEASSISTANT_SERVICES
#define USE_API_HOMEASSISTANT_STATES
#define USE_API_NOISE