Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions aioesphomeapi/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,14 @@ enum ServiceArgType {
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
}
enum SupportsResponseType {
SUPPORTS_RESPONSE_NONE = 0;
SUPPORTS_RESPONSE_OPTIONAL = 1;
SUPPORTS_RESPONSE_ONLY = 2;
// Status-only response - reports success/error without data payload
// Value is higher to avoid conflicts with future Home Assistant values
SUPPORTS_RESPONSE_STATUS = 100;
}
message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
Expand All @@ -868,6 +876,7 @@ message ListEntitiesServicesResponse {
string name = 1;
fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
Expand All @@ -890,6 +899,21 @@ message ExecuteServiceRequest {

fixed32 key = 1;
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
}

// Message sent by ESPHome to Home Assistant with service execution response data
message ExecuteServiceResponse {
option (id) = 131;
option (source) = SOURCE_SERVER;
option (no_delay) = true;
option (ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES";

uint32 call_id = 1; // Matches the call_id from ExecuteServiceRequest
bool success = 2; // Whether the service execution succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES_JSON"];
}

// ==================== CAMERA ====================
Expand Down
548 changes: 280 additions & 268 deletions aioesphomeapi/api_pb2.py

Large diffs are not rendered by default.

32 changes: 30 additions & 2 deletions aioesphomeapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
DeviceInfoResponse,
ExecuteServiceArgument,
ExecuteServiceRequest,
ExecuteServiceResponse,
FanCommandRequest,
HomeassistantActionRequest,
HomeassistantActionResponse,
Expand Down Expand Up @@ -134,6 +135,7 @@
EntityInfo,
EntityState,
ESPHomeBluetoothGATTServices,
ExecuteServiceResponse as ExecuteServiceResponseModel,
FanDirection,
FanSpeed,
HomeassistantServiceCall,
Expand Down Expand Up @@ -1312,10 +1314,21 @@ def update_command(
)

def execute_service(
self, service: UserService, data: ExecuteServiceDataType
self,
service: UserService,
data: ExecuteServiceDataType,
*,
on_response: Callable[[ExecuteServiceResponseModel], None] | None = None,
return_response: bool = False,
) -> None:
connection = self._get_connection()
req = ExecuteServiceRequest(key=service.key)
# Generate call_id when response callback is provided
call_id = next(self._call_id_counter) if on_response is not None else 0
req = ExecuteServiceRequest(
key=service.key,
call_id=call_id,
return_response=return_response,
)
args = []
apiv = self.api_version
if TYPE_CHECKING:
Expand All @@ -1339,6 +1352,21 @@ def execute_service(
# pylint: disable=no-member
req.args.extend(args)

# Register callback for response if provided
if on_response is not None:
unsub: Callable[[], None] | None = None

def _on_response(msg: ExecuteServiceResponse) -> None:
if msg.call_id == call_id:
on_response(ExecuteServiceResponseModel.from_pb(msg))
if unsub is not None:
unsub()

unsub = connection.add_message_callback(
_on_response,
(ExecuteServiceResponse,),
)

connection.send_message(req)

def _request_image(self, *, single: bool = False, stream: bool = False) -> None:
Expand Down
1 change: 1 addition & 0 deletions aioesphomeapi/client_base.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ cdef str _stringify_or_none(object value)
cdef class APIClientBase:

cdef public set _background_tasks
cdef public object _call_id_counter
cdef public APIConnection _connection
cdef public bint _debug_enabled
cdef public object _loop
Expand Down
3 changes: 3 additions & 0 deletions aioesphomeapi/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import asyncio
from collections.abc import Callable, Coroutine
import itertools
import logging
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -218,6 +219,7 @@ class APIClientBase:

__slots__ = (
"_background_tasks",
"_call_id_counter",
"_connection",
"_debug_enabled",
"_loop",
Expand Down Expand Up @@ -286,6 +288,7 @@ def __init__(
self.cached_name: str | None = None
self._background_tasks: set[asyncio.Task[Any]] = set()
self._loop = asyncio.get_running_loop()
self._call_id_counter = itertools.count(1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be added to client_base.pxd as well as object type

self._set_log_name()

def set_debug(self, enabled: bool) -> None:
Expand Down
2 changes: 2 additions & 0 deletions aioesphomeapi/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
DisconnectResponse,
EventResponse,
ExecuteServiceRequest,
ExecuteServiceResponse,
FanCommandRequest,
FanStateResponse,
GetTimeRequest,
Expand Down Expand Up @@ -491,6 +492,7 @@ def __init__(self, error: BluetoothGATTError) -> None:
128: ZWaveProxyFrame,
129: ZWaveProxyRequest,
130: HomeassistantActionResponse,
131: ExecuteServiceResponse,
}

MESSAGE_NUMBER_TO_PROTO = tuple(MESSAGE_TYPE_TO_PROTO.values())
20 changes: 20 additions & 0 deletions aioesphomeapi/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,13 @@ class UserServiceArgType(APIIntEnum):
STRING_ARRAY = 7


class SupportsResponseType(APIIntEnum):
NONE = 0
OPTIONAL = 1
ONLY = 2
STATUS = 100


@_frozen_dataclass_decorator
class UserServiceArg(APIModelBase):
name: str = ""
Expand Down Expand Up @@ -1240,6 +1247,19 @@ class UserService(APIModelBase):
args: list[UserServiceArg] = converter_field(
default_factory=list, converter=UserServiceArg.convert_list
)
supports_response: SupportsResponseType | None = converter_field(
default=SupportsResponseType.NONE, converter=SupportsResponseType.convert
)


@_frozen_dataclass_decorator
class ExecuteServiceResponse(APIModelBase):
call_id: int = 0 # Call ID that matches the original request
success: bool = False # Whether the service execution succeeded
error_message: str = "" # Error message if success = false
response_data: bytes = field(
default_factory=bytes
) # JSON response data from ESPHome


# ==================== BLUETOOTH ====================
Expand Down
Loading
Loading