23
23
from asyncio .futures import Future
24
24
from ctypes import CFUNCTYPE , c_bool , c_char_p , c_size_t , c_uint8 , c_uint16 , c_uint32 , c_void_p , py_object
25
25
from dataclasses import dataclass
26
- from typing import Type , Union
26
+ from typing import List , Optional , Type , Union
27
27
28
28
import chip .exceptions
29
29
import chip .interaction_model
@@ -42,6 +42,13 @@ class CommandPath:
42
42
CommandId : int
43
43
44
44
45
+ @dataclass
46
+ class InvokeRequestInfo :
47
+ EndpointId : int
48
+ Command : ClusterCommand
49
+ ResponseType : Optional [Type ] = None
50
+
51
+
45
52
@dataclass
46
53
class Status :
47
54
IMStatus : int
@@ -94,7 +101,11 @@ def _handleResponse(self, path: CommandPath, status: Status, response: bytes):
94
101
else :
95
102
self ._future .set_result (None )
96
103
97
- def handleResponse (self , path : CommandPath , status : Status , response : bytes ):
104
+ def handleResponse (self , path : CommandPath , index : int , status : Status , response : bytes ):
105
+ # For AsyncCommandTransaction we only expect to ever get one response so we don't bother
106
+ # checking `index`. We just share a callback API with batch commands. If we ever get a
107
+ # second call to `handleResponse` we will see a different error on trying to set future
108
+ # that has already been set.
98
109
self ._event_loop .call_soon_threadsafe (
99
110
self ._handleResponse , path , status , response )
100
111
@@ -105,9 +116,79 @@ def _handleError(self, imError: Status, chipError: PyChipError, exception: Excep
105
116
self ._future .set_exception (chipError .to_exception ())
106
117
else :
107
118
try :
119
+ # If you got an exception from this call other than AttributeError please
120
+ # add it to the except block below. We changed Exception->AttributeError as
121
+ # that is what we thought we are trying to catch here.
122
+ self ._future .set_exception (
123
+ chip .interaction_model .InteractionModelError (chip .interaction_model .Status (imError .IMStatus ), imError .ClusterStatus ))
124
+ except AttributeError :
125
+ logger .exception ("Failed to map interaction model status received: %s. Remapping to Failure." % imError )
126
+ self ._future .set_exception (chip .interaction_model .InteractionModelError (
127
+ chip .interaction_model .Status .Failure , imError .ClusterStatus ))
128
+
129
+ def handleError (self , status : Status , chipError : PyChipError ):
130
+ self ._event_loop .call_soon_threadsafe (
131
+ self ._handleError , status , chipError , None
132
+ )
133
+
134
+ def handleDone (self ):
135
+ ctypes .pythonapi .Py_DecRef (ctypes .py_object (self ))
136
+
137
+
138
+ class AsyncBatchCommandsTransaction :
139
+ def __init__ (self , future : Future , eventLoop , expectTypes : List [Type ]):
140
+ self ._event_loop = eventLoop
141
+ self ._future = future
142
+ self ._expect_types = expectTypes
143
+ default_im_failure = chip .interaction_model .InteractionModelError (
144
+ chip .interaction_model .Status .NoCommandResponse )
145
+ self ._responses = [default_im_failure ] * len (expectTypes )
146
+
147
+ def _handleResponse (self , path : CommandPath , index : int , status : Status , response : bytes ):
148
+ if index > len (self ._responses ):
149
+ self ._handleError (status , 0 , IndexError (f"CommandSenderCallback has given us an unexpected index value { index } " ))
150
+ return
151
+
152
+ if (len (response ) == 0 ):
153
+ self ._responses [index ] = None
154
+ else :
155
+ # If a type hasn't been assigned, let's auto-deduce it.
156
+ if (self ._expect_types [index ] is None ):
157
+ self ._expect_types [index ] = FindCommandClusterObject (False , path )
158
+
159
+ if self ._expect_types [index ]:
160
+ try :
161
+ # If you got an exception from this call other than AttributeError please
162
+ # add it to the except block below. We changed Exception->AttributeError as
163
+ # that is what we thought we are trying to catch here.
164
+ self ._responses [index ] = self ._expect_types [index ].FromTLV (response )
165
+ except AttributeError as ex :
166
+ self ._handleError (status , 0 , ex )
167
+ else :
168
+ self ._responses [index ] = None
169
+
170
+ def handleResponse (self , path : CommandPath , index : int , status : Status , response : bytes ):
171
+ self ._event_loop .call_soon_threadsafe (
172
+ self ._handleResponse , path , index , status , response )
173
+
174
+ def _handleError (self , imError : Status , chipError : PyChipError , exception : Exception ):
175
+ if self ._future .done ():
176
+ # TODO Right now this even callback happens if there was a real IM Status error on one command.
177
+ # We need to update OnError to allow providing a CommandRef that we can try associating with it.
178
+ logger .exception (f"Recieved another error, but we have sent error. imError:{ imError } , chipError { chipError } " )
179
+ return
180
+ if exception :
181
+ self ._future .set_exception (exception )
182
+ elif chipError != 0 :
183
+ self ._future .set_exception (chipError .to_exception ())
184
+ else :
185
+ try :
186
+ # If you got an exception from this call other than AttributeError please
187
+ # add it to the except block below. We changed Exception->AttributeError as
188
+ # that is what we thought we are trying to catch here.
108
189
self ._future .set_exception (
109
190
chip .interaction_model .InteractionModelError (chip .interaction_model .Status (imError .IMStatus ), imError .ClusterStatus ))
110
- except Exception :
191
+ except AttributeError :
111
192
logger .exception ("Failed to map interaction model status received: %s. Remapping to Failure." % imError )
112
193
self ._future .set_exception (chip .interaction_model .InteractionModelError (
113
194
chip .interaction_model .Status .Failure , imError .ClusterStatus ))
@@ -117,20 +198,29 @@ def handleError(self, status: Status, chipError: PyChipError):
117
198
self ._handleError , status , chipError , None
118
199
)
119
200
201
+ def _handleDone (self ):
202
+ self ._future .set_result (self ._responses )
203
+ ctypes .pythonapi .Py_DecRef (ctypes .py_object (self ))
204
+
205
+ def handleDone (self ):
206
+ self ._event_loop .call_soon_threadsafe (
207
+ self ._handleDone
208
+ )
209
+
120
210
121
211
_OnCommandSenderResponseCallbackFunct = CFUNCTYPE (
122
- None , py_object , c_uint16 , c_uint32 , c_uint32 , c_uint16 , c_uint8 , c_void_p , c_uint32 )
212
+ None , py_object , c_uint16 , c_uint32 , c_uint32 , c_size_t , c_uint16 , c_uint8 , c_void_p , c_uint32 )
123
213
_OnCommandSenderErrorCallbackFunct = CFUNCTYPE (
124
214
None , py_object , c_uint16 , c_uint8 , PyChipError )
125
215
_OnCommandSenderDoneCallbackFunct = CFUNCTYPE (
126
216
None , py_object )
127
217
128
218
129
219
@_OnCommandSenderResponseCallbackFunct
130
- def _OnCommandSenderResponseCallback (closure , endpoint : int , cluster : int , command : int ,
220
+ def _OnCommandSenderResponseCallback (closure , endpoint : int , cluster : int , command : int , index : int ,
131
221
imStatus : int , clusterStatus : int , payload , size ):
132
222
data = ctypes .string_at (payload , size )
133
- closure .handleResponse (CommandPath (endpoint , cluster , command ), Status (
223
+ closure .handleResponse (CommandPath (endpoint , cluster , command ), index , Status (
134
224
imStatus , clusterStatus ), data [:])
135
225
136
226
@@ -141,7 +231,7 @@ def _OnCommandSenderErrorCallback(closure, imStatus: int, clusterStatus: int, ch
141
231
142
232
@_OnCommandSenderDoneCallbackFunct
143
233
def _OnCommandSenderDoneCallback (closure ):
144
- ctypes . pythonapi . Py_DecRef ( ctypes . py_object ( closure ) )
234
+ closure . handleDone ( )
145
235
146
236
147
237
def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke (future : Future , eventLoop , responseType , device , commandPath , payload ):
@@ -201,6 +291,59 @@ def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPa
201
291
))
202
292
203
293
294
+ def SendBatchCommands (future : Future , eventLoop , device , commands : List [InvokeRequestInfo ],
295
+ timedRequestTimeoutMs : Optional [int ] = None , interactionTimeoutMs : Optional [int ] = None , busyWaitMs : Optional [int ] = None ,
296
+ suppressResponse : Optional [bool ] = None ) -> PyChipError :
297
+ ''' Send a cluster-object encapsulated command to a device and does the following:
298
+ - On receipt of a successful data response, returns the cluster-object equivalent through the provided future.
299
+ - None (on a successful response containing no data)
300
+ - Raises an exception if any errors are encountered.
301
+
302
+ If no response type is provided above, the type will be automatically deduced.
303
+
304
+ If a valid timedRequestTimeoutMs is provided, a timed interaction will be initiated instead.
305
+ If a valid interactionTimeoutMs is provided, the interaction will terminate with a CHIP_ERROR_TIMEOUT if a response
306
+ has not been received within that timeout. If it isn't provided, a sensible value will be automatically computed that
307
+ accounts for the underlying characteristics of both the transport and the responsiveness of the receiver.
308
+ '''
309
+ handle = chip .native .GetLibraryHandle ()
310
+
311
+ responseTypes = []
312
+ commandargs = []
313
+ for command in commands :
314
+ clusterCommand = command .Command
315
+ responseType = command .ResponseType
316
+ if (responseType is not None ) and (not issubclass (responseType , ClusterCommand )):
317
+ raise ValueError ("responseType must be a ClusterCommand or None" )
318
+ if clusterCommand .must_use_timed_invoke and timedRequestTimeoutMs is None or timedRequestTimeoutMs == 0 :
319
+ raise chip .interaction_model .InteractionModelError (chip .interaction_model .Status .NeedsTimedInteraction )
320
+
321
+ commandPath = chip .interaction_model .CommandPathIBStruct .build ({
322
+ "EndpointId" : command .EndpointId ,
323
+ "ClusterId" : clusterCommand .cluster_id ,
324
+ "CommandId" : clusterCommand .command_id })
325
+ payloadTLV = clusterCommand .ToTLV ()
326
+
327
+ commandargs .append (c_char_p (commandPath ))
328
+ commandargs .append (c_char_p (bytes (payloadTLV )))
329
+ commandargs .append (c_size_t (len (payloadTLV )))
330
+
331
+ responseTypes .append (responseType )
332
+
333
+ transaction = AsyncBatchCommandsTransaction (future , eventLoop , responseTypes )
334
+ ctypes .pythonapi .Py_IncRef (ctypes .py_object (transaction ))
335
+
336
+ return builtins .chipStack .Call (
337
+ lambda : handle .pychip_CommandSender_SendBatchCommands (
338
+ py_object (transaction ), device ,
339
+ c_uint16 (0 if timedRequestTimeoutMs is None else timedRequestTimeoutMs ),
340
+ c_uint16 (0 if interactionTimeoutMs is None else interactionTimeoutMs ),
341
+ c_uint16 (0 if busyWaitMs is None else busyWaitMs ),
342
+ c_bool (False if suppressResponse is None else suppressResponse ),
343
+ c_size_t (len (commands )), * commandargs )
344
+ )
345
+
346
+
204
347
def SendGroupCommand (groupId : int , devCtrl : c_void_p , payload : ClusterCommand , busyWaitMs : Union [None , int ] = None ) -> PyChipError :
205
348
''' Send a cluster-object encapsulated group command to a device and does the following:
206
349
- None (on a successful response containing no data)
@@ -227,6 +370,8 @@ def Init():
227
370
228
371
setter .Set ('pychip_CommandSender_SendCommand' ,
229
372
PyChipError , [py_object , c_void_p , c_uint16 , c_uint32 , c_uint32 , c_char_p , c_size_t , c_uint16 , c_bool ])
373
+ setter .Set ('pychip_CommandSender_SendBatchCommands' ,
374
+ PyChipError , [py_object , c_void_p , c_uint16 , c_uint16 , c_uint16 , c_bool , c_size_t ])
230
375
setter .Set ('pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke' ,
231
376
PyChipError , [py_object , c_void_p , c_uint32 , c_uint32 , c_char_p , c_size_t , c_uint16 , c_bool ])
232
377
setter .Set ('pychip_CommandSender_SendGroupCommand' ,
0 commit comments