Skip to content

Commit 41aa9c4

Browse files
committed
Enhance entity command handler with client connection
Pass the WS connection as an optional command parameter to the client. This allows the client to send directed WS events back. Otherwise, the only option is to use event broadcasts to all connected clients. Implementation is backward compatible to existing clients without the new websocket parameter in the CommandHandler callback, or extending a concrete Entity class. Import cleanup to avoid circular imports through the ucapi definition.
1 parent e56fd77 commit 41aa9c4

File tree

18 files changed

+137
-69
lines changed

18 files changed

+137
-69
lines changed

.idea/misc.xml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/hello_integration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
async def cmd_handler(
14-
entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None
14+
entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None, websocket: Any
1515
) -> ucapi.StatusCodes:
1616
"""
1717
Push button command handler.
@@ -21,6 +21,7 @@ async def cmd_handler(
2121
:param entity: button entity
2222
:param cmd_id: command
2323
:param _params: optional command parameters
24+
:param websocket: optional client connection for sending directed events
2425
:return: status of the command
2526
"""
2627
print(f"Got {entity.id} command request: {cmd_id}")

examples/remote.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747

4848
async def cmd_handler(
49-
entity: ucapi.Remote, cmd_id: str, params: dict[str, Any] | None
49+
entity: ucapi.Remote, cmd_id: str, params: dict[str, Any] | None, websocket: Any
5050
) -> ucapi.StatusCodes:
5151
"""
5252
Remote command handler.
@@ -56,6 +56,7 @@ async def cmd_handler(
5656
:param entity: remote entity
5757
:param cmd_id: command
5858
:param params: optional command parameters
59+
:param websocket: optional client connection for sending directed events
5960
:return: status of the command
6061
"""
6162
print(f"Got {entity.id} command request: {cmd_id}")

examples/setup_flow.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ async def handle_user_data_response(msg: ucapi.UserDataResponse) -> ucapi.SetupA
157157

158158

159159
async def cmd_handler(
160-
entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None
160+
entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None, websocket: Any
161161
) -> ucapi.StatusCodes:
162162
"""
163163
Push button command handler.
@@ -167,6 +167,7 @@ async def cmd_handler(
167167
:param entity: button entity
168168
:param cmd_id: command
169169
:param _params: optional command parameters
170+
:param websocket: optional client connection for sending directed events
170171
:return: status of the command
171172
"""
172173
print(f"Got {entity.id} command request: {cmd_id}")

examples/voice.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ async def on_subscribe_entities(entity_ids: list[str]) -> None:
4545

4646

4747
async def on_voice_cmd(
48-
entity: ucapi.VoiceAssistant, cmd_id: str, params: dict[str, Any] | None
48+
entity: ucapi.VoiceAssistant,
49+
cmd_id: str,
50+
params: dict[str, Any] | None,
51+
websocket: Any,
4952
) -> ucapi.StatusCodes:
5053
"""
5154
Voice assistant command handler.
@@ -55,6 +58,7 @@ async def on_voice_cmd(
5558
:param entity: voice assistant entity
5659
:param cmd_id: command
5760
:param params: optional command parameters
61+
:param websocket: optional client connection for sending directed events
5862
:return: status of the command
5963
"""
6064
# HACK until core is fixed

ucapi/api.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -589,10 +589,10 @@ async def broadcast_assistant_event(self, event: uc.AssistantEvent) -> None:
589589
uc.EventCategory.ENTITY,
590590
)
591591

592-
async def send_assistant_event(self, websocket, event: uc.AssistantEvent) -> None:
593-
"""Send an assistant event to the given WebSocket client."""
592+
async def send_assistant_event(self, client, event: uc.AssistantEvent) -> None:
593+
"""Send an assistant event to the given client connection."""
594594
await self._send_ws_event(
595-
websocket,
595+
client,
596596
uc.WsMsgEvents.ASSISTANT_EVENT,
597597
asdict(event),
598598
uc.EventCategory.ENTITY,
@@ -888,9 +888,22 @@ async def _entity_command(
888888
# Start timeout immediately on session creation
889889
self._schedule_voice_timeout(key)
890890

891-
result = await entity.command(
892-
cmd_id, msg_data["params"] if "params" in msg_data else None
893-
)
891+
try:
892+
result = await entity.command(
893+
cmd_id,
894+
msg_data["params"] if "params" in msg_data else None,
895+
websocket=websocket,
896+
)
897+
except TypeError:
898+
# Entity command method likely doesn't accept 'websocket' kwarg -> legacy handler signature.
899+
_LOG.warning(
900+
"Old Entity.command signature detected for %s, trying old signature. Please update the command signature.",
901+
entity.id,
902+
)
903+
result = await entity.command(
904+
cmd_id, msg_data["params"] if "params" in msg_data else None
905+
)
906+
894907
await self.acknowledge_command(websocket, req_id, result)
895908

896909
async def _setup_driver(

ucapi/api_definitions.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,19 @@ class SetupComplete(SetupAction):
205205

206206

207207
CommandHandler: TypeAlias = Callable[
208-
[Any, str, dict[str, Any] | None], Awaitable[StatusCodes]
208+
[Any, str, dict[str, Any] | None, Any | None], Awaitable[StatusCodes]
209209
]
210+
"""Entity command handler signature.
211+
212+
Parameters:
213+
214+
- entity: entity instance
215+
- cmd_id: command identifier
216+
- params: optional command parameters
217+
- websocket: optional client connection for sending directed events
218+
219+
Returns: status code
220+
"""
210221

211222

212223
SetupHandler: TypeAlias = Callable[[SetupDriver], Awaitable[SetupAction]]

ucapi/button.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
from enum import Enum
99

10-
from ucapi.api_definitions import CommandHandler
11-
from ucapi.entity import Entity, EntityTypes
10+
from .api_definitions import CommandHandler
11+
from .entity import Entity, EntityTypes
1212

1313

1414
class States(str, Enum):

ucapi/climate.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from enum import Enum
99
from typing import Any
1010

11-
from ucapi.api_definitions import CommandHandler
12-
from ucapi.entity import Entity, EntityTypes
11+
from .api_definitions import CommandHandler
12+
from .entity import Entity, EntityTypes
1313

1414

1515
class States(str, Enum):
@@ -111,8 +111,8 @@ def __init__(
111111
EntityTypes.CLIMATE,
112112
features,
113113
attributes,
114-
device_class,
115-
options,
116-
area,
117-
cmd_handler,
114+
device_class=device_class,
115+
options=options,
116+
area=area,
117+
cmd_handler=cmd_handler,
118118
)

ucapi/cover.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from enum import Enum
99
from typing import Any
1010

11-
from ucapi.api_definitions import CommandHandler
12-
from ucapi.entity import Entity, EntityTypes
11+
from .api_definitions import CommandHandler
12+
from .entity import Entity, EntityTypes
1313

1414

1515
class States(str, Enum):
@@ -110,8 +110,8 @@ def __init__(
110110
EntityTypes.COVER,
111111
features,
112112
attributes,
113-
device_class,
114-
options,
115-
area,
116-
cmd_handler,
113+
device_class=device_class,
114+
options=options,
115+
area=area,
116+
cmd_handler=cmd_handler,
117117
)

0 commit comments

Comments
 (0)