mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		@@ -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,
 | 
			
		||||
@@ -17,30 +18,50 @@ from esphome.const import (
 | 
			
		||||
    CONF_MAX_CONNECTIONS,
 | 
			
		||||
    CONF_ON_CLIENT_CONNECTED,
 | 
			
		||||
    CONF_ON_CLIENT_DISCONNECTED,
 | 
			
		||||
    CONF_ON_ERROR,
 | 
			
		||||
    CONF_ON_SUCCESS,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
    CONF_PORT,
 | 
			
		||||
    CONF_REBOOT_TIMEOUT,
 | 
			
		||||
    CONF_RESPONSE_TEMPLATE,
 | 
			
		||||
    CONF_SERVICE,
 | 
			
		||||
    CONF_SERVICES,
 | 
			
		||||
    CONF_TAG,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_VARIABLES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_generator import TemplateArgsType
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
DOMAIN = "api"
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
AUTO_LOAD = ["socket"]
 | 
			
		||||
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_(
 | 
			
		||||
    "HomeAssistantServiceCallAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
ActionResponse = api_ns.class_("ActionResponse")
 | 
			
		||||
HomeAssistantActionResponseTrigger = api_ns.class_(
 | 
			
		||||
    "HomeAssistantActionResponseTrigger", automation.Trigger
 | 
			
		||||
)
 | 
			
		||||
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
 | 
			
		||||
 | 
			
		||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
 | 
			
		||||
@@ -288,6 +309,29 @@ async def to_code(config):
 | 
			
		||||
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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_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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -303,10 +347,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
 | 
			
		||||
                {cv.string: cv.returning_lambda}
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
 | 
			
		||||
            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),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
 | 
			
		||||
    cv.rename_key(CONF_SERVICE, CONF_ACTION),
 | 
			
		||||
    _validate_response_config,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -320,7 +369,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
    HomeAssistantServiceCallAction,
 | 
			
		||||
    HOMEASSISTANT_ACTION_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def homeassistant_service_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
):
 | 
			
		||||
    cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
 | 
			
		||||
    serv = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, serv, False)
 | 
			
		||||
@@ -335,6 +389,40 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    for key, value in config[CONF_VARIABLES].items():
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_variable(key, templ))
 | 
			
		||||
 | 
			
		||||
    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_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.JsonObjectConst, "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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -780,6 +780,22 @@ 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"];
 | 
			
		||||
  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
 | 
			
		||||
message HomeassistantActionResponse {
 | 
			
		||||
  option (id) = 130;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
  option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
 | 
			
		||||
 | 
			
		||||
  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, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== IMPORT HOME ASSISTANT STATES ====================
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,9 @@
 | 
			
		||||
#endif
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include "esphome/components/network/util.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
@@ -1549,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
 | 
			
		||||
#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
 | 
			
		||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
 | 
			
		||||
  NoiseEncryptionSetKeyResponse resp;
 | 
			
		||||
 
 | 
			
		||||
@@ -129,7 +129,10 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
      return;
 | 
			
		||||
    this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
 | 
			
		||||
  void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
 | 
			
		||||
 
 | 
			
		||||
@@ -884,6 +884,15 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
    buffer.encode_message(4, it, true);
 | 
			
		||||
  }
 | 
			
		||||
  buffer.encode_bool(5, this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  buffer.encode_uint32(6, this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#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 {
 | 
			
		||||
  size.add_length(1, this->service_ref_.size());
 | 
			
		||||
@@ -891,6 +900,48 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_repeated_message(1, this->data_template);
 | 
			
		||||
  size.add_repeated_message(1, this->variables);
 | 
			
		||||
  size.add_bool(1, this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  size.add_uint32(1, this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#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
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      this->call_id = value.as_uint32();
 | 
			
		||||
      break;
 | 
			
		||||
    case 2:
 | 
			
		||||
      this->success = value.as_bool();
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = 113;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 128;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "homeassistant_action_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1114,6 +1114,15 @@ class HomeassistantActionRequest final : public ProtoMessage {
 | 
			
		||||
  std::vector<HomeassistantServiceMap> data_template{};
 | 
			
		||||
  std::vector<HomeassistantServiceMap> variables{};
 | 
			
		||||
  bool is_event{false};
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  uint32_t call_id{0};
 | 
			
		||||
#endif
 | 
			
		||||
#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;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -1123,6 +1132,30 @@ class HomeassistantActionRequest final : public ProtoMessage {
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
class HomeassistantActionResponse final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 130;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 34;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "homeassistant_action_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
 
 | 
			
		||||
@@ -1122,6 +1122,28 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
 | 
			
		||||
    out.append("\n");
 | 
			
		||||
  }
 | 
			
		||||
  dump_field(out, "is_event", this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  dump_field(out, "call_id", this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#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
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void HomeassistantActionResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "HomeassistantActionResponse");
 | 
			
		||||
  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
 | 
			
		||||
 
 | 
			
		||||
@@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
      this->on_z_wave_proxy_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
    case HomeassistantActionResponse::MESSAGE_TYPE: {
 | 
			
		||||
      HomeassistantActionResponse msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_homeassistant_action_response(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
  virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
  virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -9,12 +9,16 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#include "homeassistant_service.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
@@ -400,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
 | 
			
		||||
    client->send_homeassistant_action(call);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
 | 
			
		||||
  this->action_response_callbacks_.push_back({call_id, std::move(callback)});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
 | 
			
		||||
  for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
 | 
			
		||||
    if (it->call_id == call_id) {
 | 
			
		||||
      auto callback = std::move(it->callback);
 | 
			
		||||
      this->action_response_callbacks_.erase(it);
 | 
			
		||||
      ActionResponse response(success, error_message);
 | 
			
		||||
      callback(response);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#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) {
 | 
			
		||||
  for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
 | 
			
		||||
    if (it->call_id == call_id) {
 | 
			
		||||
      auto callback = std::move(it->callback);
 | 
			
		||||
      this->action_response_callbacks_.erase(it);
 | 
			
		||||
      ActionResponse response(success, error_message, response_data, response_data_len);
 | 
			
		||||
      callback(response);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@
 | 
			
		||||
#include "user_services.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
@@ -111,7 +112,17 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
  void send_homeassistant_action(const HomeassistantActionRequest &call);
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  // Action response handling
 | 
			
		||||
  using ActionResponseCallback = std::function<void(const 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
 | 
			
		||||
  void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -187,6 +198,13 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  std::vector<UserServiceDescriptor *> user_services_;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  struct PendingActionResponse {
 | 
			
		||||
    uint32_t call_id;
 | 
			
		||||
    ActionResponseCallback callback;
 | 
			
		||||
  };
 | 
			
		||||
  std::vector<PendingActionResponse> action_response_callbacks_;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Group smaller types together
 | 
			
		||||
  uint16_t port_{6053};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,13 @@
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#ifdef USE_API
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#include "esphome/components/json/json_util.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
@@ -44,9 +49,47 @@ template<typename... Ts> class TemplatableKeyValuePair {
 | 
			
		||||
  TemplatableStringValue<Ts...> value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
// Represents the response data from a Home Assistant action
 | 
			
		||||
class ActionResponse {
 | 
			
		||||
 public:
 | 
			
		||||
  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);
 | 
			
		||||
  }
 | 
			
		||||
#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 (const version returns read-only view)
 | 
			
		||||
  JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool success_;
 | 
			
		||||
  std::string error_message_;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  JsonDocument json_document_;
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Callback type for action responses
 | 
			
		||||
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
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; }
 | 
			
		||||
 | 
			
		||||
@@ -61,11 +104,29 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
 | 
			
		||||
    this->variables_.emplace_back(std::move(key), value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  template<typename T> void set_response_template(T response_template) {
 | 
			
		||||
    this->response_template_ = response_template;
 | 
			
		||||
    this->flags_.has_response_template = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_wants_status() { this->flags_.wants_status = true; }
 | 
			
		||||
  void set_wants_response() { this->flags_.wants_response = true; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  Trigger<JsonObjectConst, 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
 | 
			
		||||
 | 
			
		||||
  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();
 | 
			
		||||
@@ -84,18 +145,74 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
 | 
			
		||||
      kv.set_key(StringRef(it.key));
 | 
			
		||||
      kv.value = it.value.value(x...);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
    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;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
      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;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
      auto captured_args = std::make_tuple(x...);
 | 
			
		||||
      this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) {
 | 
			
		||||
        std::apply(
 | 
			
		||||
            [this, &response](auto &&...args) {
 | 
			
		||||
              if (response.is_success()) {
 | 
			
		||||
#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...);
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                this->error_trigger_->trigger(response.get_error_message(), args...);
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            captured_args);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    this->parent_->send_homeassistant_action(resp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 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<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, 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  // 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
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -677,6 +677,7 @@ CONF_ON_RESPONSE = "on_response"
 | 
			
		||||
CONF_ON_SHUTDOWN = "on_shutdown"
 | 
			
		||||
CONF_ON_SPEED_SET = "on_speed_set"
 | 
			
		||||
CONF_ON_STATE = "on_state"
 | 
			
		||||
CONF_ON_SUCCESS = "on_success"
 | 
			
		||||
CONF_ON_TAG = "on_tag"
 | 
			
		||||
CONF_ON_TAG_REMOVED = "on_tag_removed"
 | 
			
		||||
CONF_ON_TIME = "on_time"
 | 
			
		||||
@@ -819,6 +820,7 @@ CONF_RESET_DURATION = "reset_duration"
 | 
			
		||||
CONF_RESET_PIN = "reset_pin"
 | 
			
		||||
CONF_RESIZE = "resize"
 | 
			
		||||
CONF_RESOLUTION = "resolution"
 | 
			
		||||
CONF_RESPONSE_TEMPLATE = "response_template"
 | 
			
		||||
CONF_RESTART = "restart"
 | 
			
		||||
CONF_RESTORE = "restore"
 | 
			
		||||
CONF_RESTORE_MODE = "restore_mode"
 | 
			
		||||
 
 | 
			
		||||
@@ -112,6 +112,8 @@
 | 
			
		||||
#define USE_API
 | 
			
		||||
#define USE_API_CLIENT_CONNECTED_TRIGGER
 | 
			
		||||
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
 | 
			
		||||
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#define USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#define USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
#define USE_API_NOISE
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,39 @@ esphome:
 | 
			
		||||
          data:
 | 
			
		||||
            message: Button was pressed
 | 
			
		||||
      - homeassistant.tag_scanned: pulse
 | 
			
		||||
      - homeassistant.action:
 | 
			
		||||
          action: weather.get_forecasts
 | 
			
		||||
          data:
 | 
			
		||||
            entity_id: weather.forecast_home
 | 
			
		||||
            type: hourly
 | 
			
		||||
          capture_response: true
 | 
			
		||||
          on_success:
 | 
			
		||||
            - lambda: |-
 | 
			
		||||
                JsonObjectConst next_hour = response["response"]["weather.forecast_home"]["forecast"][0];
 | 
			
		||||
                float next_temperature = next_hour["temperature"].as<float>();
 | 
			
		||||
                ESP_LOGD("main", "Next hour temperature: %f", next_temperature);
 | 
			
		||||
          on_error:
 | 
			
		||||
            - lambda: |-
 | 
			
		||||
                ESP_LOGE("main", "Action failed with error: %s", error.c_str());
 | 
			
		||||
      - homeassistant.action:
 | 
			
		||||
          action: weather.get_forecasts
 | 
			
		||||
          data:
 | 
			
		||||
            entity_id: weather.forecast_home
 | 
			
		||||
            type: hourly
 | 
			
		||||
          capture_response: true
 | 
			
		||||
          response_template: "{{ response['weather.forecast_home']['forecast'][0]['temperature'] }}"
 | 
			
		||||
          on_success:
 | 
			
		||||
            - lambda: |-
 | 
			
		||||
                float temperature = response["response"].as<float>();
 | 
			
		||||
                ESP_LOGD("main", "Next hour temperature: %f", temperature);
 | 
			
		||||
      - homeassistant.action:
 | 
			
		||||
          action: light.toggle
 | 
			
		||||
          data:
 | 
			
		||||
            entity_id: light.demo_light
 | 
			
		||||
          on_success:
 | 
			
		||||
            - logger.log: "Toggled demo light"
 | 
			
		||||
          on_error:
 | 
			
		||||
            - logger.log: "Failed to toggle demo light"
 | 
			
		||||
 | 
			
		||||
api:
 | 
			
		||||
  port: 8000
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user