Skip to content

Commit

Permalink
Auto-Deduce Invoke Response Type (#10933)
Browse files Browse the repository at this point in the history
* Auto-Deduce Invoke Response Type

This modifies the existing ChipDeviceController.SendCommand() Python API
to automatically deduce the cluster object type associated with the
response and automatically return that to the caller after successfully
deserializing the TLV into that object.

This avoids callers having to explicitly pass in an object, making it
that much easier to use.

Tests: Tested using cluster_objects.py, as well as manually
sending/receiving commands at the Python shell.

* Fix test case

* Restyle

Co-authored-by: Song Guo <songguo@google.com>
  • Loading branch information
2 people authored and pull[bot] committed May 2, 2022
1 parent 59b215b commit 2529872
Show file tree
Hide file tree
Showing 8 changed files with 2,341 additions and 1,909 deletions.
6 changes: 6 additions & 0 deletions src/controller/python/chip/ChipDeviceCtrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,12 @@ def DeviceAvailableCallback(device, err):
return returnDevice

async def SendCommand(self, nodeid: int, endpoint: int, payload: ClusterObjects.ClusterCommand, responseType=None):
'''
Send a cluster-object encapsulated command to a node and get returned a future that can be awaited upon to receive the response.
If a valid responseType is passed in, that will be used to deserialize the object. If not, the type will be automatically deduced
from the metadata received over the wire.
'''

eventLoop = asyncio.get_running_loop()
future = eventLoop.create_future()

Expand Down
60 changes: 49 additions & 11 deletions src/controller/python/chip/clusters/Command.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
from typing import Type
from ctypes import CFUNCTYPE, c_char_p, c_size_t, c_void_p, c_uint32, c_uint16, c_uint8, py_object


from .ClusterObjects import ClusterCommand
import chip.exceptions
import chip.interaction_model

import inspect
import sys


@dataclass
class CommandPath:
Expand All @@ -40,27 +42,56 @@ class Status:
ClusterStatus: int


def FindCommandClusterObject(isClientSideCommand: bool, path: CommandPath):
''' Locates the right generated cluster object given a set of parameters.
isClientSideCommand: True if it is a client-to-server command, else False.
path: A CommandPath that describes the endpoint, cluster and ID of the command.
Returns the type of the cluster object if one is found. Otherwise, returns None.
'''
for clusterName, obj in inspect.getmembers(sys.modules['chip.clusters.Objects']):
if ('chip.clusters.Objects' in str(obj)) and inspect.isclass(obj):
for objName, subclass in inspect.getmembers(obj):
if inspect.isclass(subclass) and (('Commands') in str(subclass)):
for commandName, command in inspect.getmembers(subclass):
if inspect.isclass(command):
for name, field in inspect.getmembers(command):
if ('__dataclass_fields__' in name):
if (field['cluster_id'].default == path.ClusterId) and (field['command_id'].default == path.CommandId) and (field['is_client'].default == isClientSideCommand):
return eval('chip.clusters.Objects.' + clusterName + '.Commands.' + commandName)
return None


class AsyncCommandTransaction:
def __init__(self, future: Future, eventLoop, expectType: Type):
self._event_loop = eventLoop
self._future = future
self._expect_type = expectType

def _handleResponse(self, status: Status, response: bytes):
if self._expect_type:
try:
self._future.set_result(self._expect_type.FromTLV(response))
except Exception as ex:
self._handleError(
status, 0, ex)
else:
def _handleResponse(self, path: CommandPath, status: Status, response: bytes):
if (len(response) == 0):
self._future.set_result(None)
else:
# If a type hasn't been assigned, let's auto-deduce it.
if (self._expect_type == None):
self._expect_type = FindCommandClusterObject(False, path)

if self._expect_type:
try:
self._future.set_result(
self._expect_type.FromTLV(response))
except Exception as ex:
self._handleError(
status, 0, ex)
else:
self._future.set_result(None)

def handleResponse(self, path: CommandPath, status: Status, response: bytes):
self._event_loop.call_soon_threadsafe(
self._handleResponse, status, response)
self._handleResponse, path, status, response)

def _handleError(self, status: Status, chipError: int, exception: Exception):
def _handleError(self, imError: int, chipError: int, exception: Exception):
if exception:
self._future.set_exception(exception)
elif chipError != 0 and chipError != 0xCA:
Expand Down Expand Up @@ -107,6 +138,13 @@ def _OnCommandSenderDoneCallback(closure):


def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPath: CommandPath, payload: ClusterCommand) -> int:
''' Send a cluster-object encapsulated command to a device and does the following:
- On receipt of a successful data response, returns the cluster-object equivalent through the provided future.
- None (on a successful response containing no data)
- Raises an exception if any errors are encountered.
If no response type is provided above, the type will be automatically deduced.
'''
if (responseType is not None) and (not issubclass(responseType, ClusterCommand)):
raise ValueError("responseType must be a ClusterCommand or None")

Expand Down
Loading

0 comments on commit 2529872

Please sign in to comment.