diff --git a/README.md b/README.md index b08de6e..0659985 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ GDM runs on the test host and communicates with the physical devices via one or more device transports (such as SSH, ADB, HTTPS, UART). GDM does not require any additional support from the device firmware. -GDM is used for on-device testing at Google Nest. +GDM is used for on-device testing at Google Gazoo. ## Table of contents @@ -122,7 +122,7 @@ Installation steps: To enable flashing ESP32 dev boards through GDM, install `esptool`: ```shell - ~/gazoo/gdm/virtual_env/bin/pip install esptool>=3.2 + ~/gazoo/gdm/virtual_env/bin/pip install esptool==4.1 ``` GDM installation creates a virtual environment for the CLI at diff --git a/docs/Matter_endpoints.md b/docs/Matter_endpoints.md index a7bd942..2ce3717 100644 --- a/docs/Matter_endpoints.md +++ b/docs/Matter_endpoints.md @@ -68,8 +68,8 @@ the device will raise a `DeviceError`: ``` >>> nrf.door_lock -nrfmatter-4585 starting MatterEndpointsAccessor.get_endpoint_instance_by_class(endpoint_class=) -nrfmatter-4585 starting MatterEndpointsAccessor.get_endpoint_id(endpoint_class=) +nrfmatter-4585 starting MatterEndpointsAccessorPwRpc.get_endpoint_instance_by_class(endpoint_class=) +nrfmatter-4585 starting MatterEndpointsAccessorPwRpc.get_endpoint_id(endpoint_class=) Traceback (most recent call last): .... raise errors.DeviceError( diff --git a/docs/device_setup/ESP32_Matter_sample_app.md b/docs/device_setup/ESP32_Matter_sample_app.md index b484516..5ba480b 100644 --- a/docs/device_setup/ESP32_Matter_sample_app.md +++ b/docs/device_setup/ESP32_Matter_sample_app.md @@ -276,8 +276,8 @@ Assume `esp` does not have a `DoorLock` endpoint. ``` >>> esp.door_lock -esp32matter-4585 starting MatterEndpointsAccessor.get_endpoint_instance_by_class(endpoint_class=) -esp32matter-4585 starting MatterEndpointsAccessor.get_endpoint_id(endpoint_class=) +esp32matter-4585 starting MatterEndpointsAccessorPwRpc.get_endpoint_instance_by_class(endpoint_class=) +esp32matter-4585 starting MatterEndpointsAccessorPwRpc.get_endpoint_id(endpoint_class=) Traceback (most recent call last): .... raise errors.DeviceError( diff --git a/docs/device_setup/NRF_Matter_sample_app.md b/docs/device_setup/NRF_Matter_sample_app.md index c15bdcf..71065c1 100644 --- a/docs/device_setup/NRF_Matter_sample_app.md +++ b/docs/device_setup/NRF_Matter_sample_app.md @@ -243,8 +243,8 @@ Assume `nrf` does not have a `DoorLock` endpoint. ``` >>> nrf.door_lock -nrfmatter-4585 starting MatterEndpointsAccessor.get_endpoint_instance_by_class(endpoint_class=) -nrfmatter-4585 starting MatterEndpointsAccessor.get_endpoint_id(endpoint_class=) +nrfmatter-4585 starting MatterEndpointsAccessorPwRpc.get_endpoint_instance_by_class(endpoint_class=) +nrfmatter-4585 starting MatterEndpointsAccessorPwRpc.get_endpoint_id(endpoint_class=) Traceback (most recent call last): .... raise errors.DeviceError( diff --git a/docs/device_setup/Raspberry_Pi_as_matter_controller.md b/docs/device_setup/Raspberry_Pi_as_matter_controller.md new file mode 100644 index 0000000..7563d34 --- /dev/null +++ b/docs/device_setup/Raspberry_Pi_as_matter_controller.md @@ -0,0 +1,110 @@ +# GDM device setup: Raspberry Pi (as a Matter controller) + +Supported models: Raspberry Pi 4 with at least 4GB of memory. + +Supported kernel images: Ubuntu 21.04 or later. + +## Setup + +1. Follow the instructions from + [Installing prerequisites on Raspberry Pi 4](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/BUILDING.md#installing-prerequisites-on-raspberry-pi-4) + section of + [project-chip/connectedhomeip](https://github.com/project-chip/connectedhomeip)'s + Building Matter guide to set up the Raspberry Pi. +2. On the raspberry pi, build `chip-tool` and install it to + `/usr/local/bin/chip-tool`. Follow the instructions on + https://github.com/project-chip/connectedhomeip/tree/master/examples/chip-tool#building-the-example-application. + +3. On the raspberry pi, save the commit SHA the `chip-tool` was built at by + running + + ```shell + echo $COMMIT_SHA > ~/.matter_sdk_version + ``` + +4. Ensure that user `pi` can write to `/usr/local/bin` directory. This is + required for `matter_controller.upgrade` functionality to work properly. Run + the command below on the raspberry pi: + + ```shell + sudo chown -R `whoami`:pi /usr/local/bin + ``` + +5. Follow the instructions from + [GDM device setup: Raspberry Pi (as a support device)](./Raspberry_Pi_as_supporting_device.md) + starting from the `Configure GDM SSH keys` step to ensure that gdm can + detect the raspberry pi as a `rpi_matter_controller`. The output of `gdm + devices` command should look like: + + ```shell + $ gdm devices + Device Alias Type Model Connected + ------------------------------ --------------- ------------------------ -------------------- ---------- + + Other Devices Alias Type Model Available + ------------------------------ --------------- ------------------------ -------------------- ---------- + rpi_matter_controller-1234 rpi_matter_controller 4 Model B Rev 1.4 available + ``` + +### Optional: Thread Border Router Setup + +Requirement: +[NRF52840-DONGLE](https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle) + +1. On the host machine, follow the instructions from + [project-chip/connectedhomeip](https://github.com/project-chip/connectedhomeip)'s + [Configuring OpenThread Radio Co-processor on nRF52840 Dongle](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/openthread_rcp_nrf_dongle.md) + page to build and install the NRF52840-DONGLE RCP firmware. + +2. Plug the NRF52840-DONGLE to the raspberry pi. + +3. On the raspberry pi, install the OpenThread Border Router following Step 2 + and 3 from this + [instructions](https://openthread.io/codelabs/openthread-border-router#0). + + In Step 2 “Setup OTBR”, use eth0 as the infrastructure network as the + Raspberry Pi should be connected to the WiFi router via the ethernet (eth0) + interface: + + ```shell + INFRA_IF_NAME=eth0 ./script/setup + ``` + + In + [Step 2 “Setup OTBR”](https://openthread.io/codelabs/openthread-border-router#1), + skip the part about flashing the RCP firmware if this was done successfully + in step 1 above. + + After + [Step 3 “Form a Thread Network”](https://openthread.io/codelabs/openthread-border-router#1), + you can stop - you have created a Thread network and are ready to commission + CHIP devices onto it. Note the thread dataset as it will be needed to + commission CHIP devices onto the network: + + ```shell + sudo ot-ctl dataset active -x + + 0e080000000000010000000300001835060004001fffe00208161de905837b6ba10708fdd61eb482e203ad0510fe8c68576cef838b184b41df13c9e694030f4f70656e5468726561642d323832610102282a0410615a57bd3d170a24ac2a461d37c8e97c0c0402a0fff8 + ``` + +## Usage + +```shell +# Commission a device on network with 20202021 setup code and assign it node id 100. +gdm issue rpi_matter_controller-1234 - matter_controller commission 100 20202021 + +# Commission a device over thread using dataset generated from `Optional: Thread Border Router Setup` step +gdm issue rpi_matter_controller-1234 - matter_controller commission 100 20202021 --operational_dataset 0e080000000000010000000300001835060004001fffe00208161de905837b6ba10708fdd61eb482e203ad0510fe8c68576cef838b184b41df13c9e694030f4f70656e5468726561642d323832610102282a0410615a57bd3d170a24ac2a461d37c8e97c0c0402a0fff8 + +# Send a toggle command to the device's onoff cluster at endpoint 1. +gdm issue rpi_matter_controller-1234 - matter_controller send 100 1 onoff toggle [] + +# Upgrade chip-tool version using a binary from the host machine. +gdm issue rpi_matter_controller-1234 - matter_controller upgrade --build_file=/path/to/chip-tool --build_id=$COMMIT_SHA + +# Print commit SHA the chip-tool was built at. +gdm issue rpi_matter_controller-1234 - matter_controller version + +# To see all supported functionality +gdm man rpi_matter_controller +``` diff --git a/docs/device_setup/Raspberry_Pi_as_supporting_device.md b/docs/device_setup/Raspberry_Pi_as_supporting_device.md index c8720a7..f2ffc0a 100644 --- a/docs/device_setup/Raspberry_Pi_as_supporting_device.md +++ b/docs/device_setup/Raspberry_Pi_as_supporting_device.md @@ -50,3 +50,25 @@ echo "Example file" > /tmp/foo.txt gdm issue raspberrypi-1234 - file_transfer - send_file_to_device --src=/tmp/foo.txt --dest=/tmp gdm man raspberrypi # To see all supported functionality ``` + +## Troubleshooting + +If `gdm detect` detection fails (couldn't recognize your Raspberry Pi), check +if your device is still pingable first: + +``` +ping +``` + +If it's alive, try the following commands on your host: + +``` +ssh -T -oPasswordAuthentication=no -oStrictHostKeyChecking=no -oBatchMode=yes -oConnectTimeout=3 -i /gazoo/gdm/keys/gazoo_device_controllers/raspberrypi3_ssh_key pi@ +``` + +If it shows permission denied failure, you'll need to manually add the new host +key to RPi's authorized keys. + +``` +ssh-copy-id -i ~/gazoo/gdm/keys/gazoo_device_controllers/raspberrypi3_ssh_key.pub pi@ +``` diff --git a/gazoo_device/_version.py b/gazoo_device/_version.py index a4fb8b4..00e9fa8 100644 --- a/gazoo_device/_version.py +++ b/gazoo_device/_version.py @@ -13,4 +13,4 @@ # limitations under the License. """Gazoo Device Manager version.""" -version = "1.72.0" +version = "1.75.0" diff --git a/gazoo_device/auxiliary_devices/raspberry_pi_matter_controller.py b/gazoo_device/auxiliary_devices/raspberry_pi_matter_controller.py index e29f8c3..55b760a 100644 --- a/gazoo_device/auxiliary_devices/raspberry_pi_matter_controller.py +++ b/gazoo_device/auxiliary_devices/raspberry_pi_matter_controller.py @@ -14,11 +14,16 @@ """Raspberry Pi Matter Controller device class.""" +from typing import Optional + from gazoo_device import decorators from gazoo_device import detect_criteria +from gazoo_device import errors from gazoo_device import gdm_logger from gazoo_device.auxiliary_devices import raspberry_pi from gazoo_device.capabilities import matter_controller_chip_tool +from gazoo_device.capabilities import matter_endpoints_accessor_chip_tool +from gazoo_device.capabilities.matter_endpoints import on_off_light logger = gdm_logger.get_logger() @@ -42,4 +47,45 @@ def matter_controller( device_name=self.name, regex_shell_fn=self.shell_with_regex, shell_fn=self.shell, - send_file_to_device=self.file_transfer.send_file_to_device) + send_file_to_device=self.file_transfer.send_file_to_device, + get_property_fn=self.get_property, + set_property_fn=self.get_manager().set_prop) + + @decorators.OptionalProperty + def matter_node_id(self) -> Optional[int]: + """Matter Node ID assigned to the currently commissioned end device.""" + return self.props["optional"].get("matter_node_id") + + @decorators.CapabilityDecorator( + matter_endpoints_accessor_chip_tool.MatterEndpointsAccessorChipTool) + def matter_endpoints( + self + ) -> matter_endpoints_accessor_chip_tool.MatterEndpointsAccessorChipTool: + """Matter capability to access commissioned device's endpoint instances.""" + if self.matter_node_id is None: + raise errors.DeviceError( + "matter_endpoints requires a commissioned end device.") + + return self.lazy_init( + matter_endpoints_accessor_chip_tool.MatterEndpointsAccessorChipTool, + device_name=self.name, + node_id_getter=lambda: self.matter_node_id, + shell_fn=self.shell, + shell_with_regex=self.shell_with_regex, + matter_controller=self.matter_controller) + + # ******************** Matter endpoint aliases ******************** # + + @decorators.CapabilityDecorator(on_off_light.OnOffLightEndpoint) + def on_off_light(self) -> on_off_light.OnOffLightEndpoint: + """Matter OnOff Light endpoint instance. + + Returns: + OnOff Light endpoint instance. + + Raises: + DeviceError when OnOff Light endpoint is not supported on the + device. + """ + return self.matter_endpoints.get_endpoint_instance_by_class( + on_off_light.OnOffLightEndpoint) diff --git a/gazoo_device/base_classes/matter_device_base.py b/gazoo_device/base_classes/matter_device_base.py index 1264a46..282fec0 100644 --- a/gazoo_device/base_classes/matter_device_base.py +++ b/gazoo_device/base_classes/matter_device_base.py @@ -22,7 +22,7 @@ from gazoo_device import gdm_logger from gazoo_device.base_classes import gazoo_device_base from gazoo_device.capabilities import device_power_default -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities import pwrpc_button_default from gazoo_device.capabilities import pwrpc_common_default from gazoo_device.capabilities.matter_endpoints import color_temperature_light @@ -255,12 +255,12 @@ def pw_rpc_common(self) -> pwrpc_common_default.PwRPCCommonDefault: rpc_timeout_s=_RPC_TIMEOUT) @decorators.CapabilityDecorator( - matter_endpoints_accessor.MatterEndpointsAccessor) + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc) def matter_endpoints( - self) -> matter_endpoints_accessor.MatterEndpointsAccessor: + self) -> matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc: """Generic Matter endpoint instance.""" return self.lazy_init( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, device_name=self.name, switchboard_call=self.switchboard.call, rpc_timeout_s=_RPC_TIMEOUT diff --git a/gazoo_device/capabilities/flash_build_esptool.py b/gazoo_device/capabilities/flash_build_esptool.py index c228300..8c3f431 100644 --- a/gazoo_device/capabilities/flash_build_esptool.py +++ b/gazoo_device/capabilities/flash_build_esptool.py @@ -26,18 +26,12 @@ logger = gdm_logger.get_logger() -# Import path differs for esptool internally and externally. try: # pylint: disable=g-import-not-at-top - from esptool import esptool + import esptool _ESPTOOL_AVAILABLE = True except ImportError: - try: - # pylint: disable=g-import-not-at-top - import esptool - _ESPTOOL_AVAILABLE = True - except ImportError: - _ESPTOOL_AVAILABLE = False + _ESPTOOL_AVAILABLE = False _DEFAULT_BOOT_UP_TIMEOUT_SECONDS = 30 _SWITCHBOARD_CAPABILITY = 'switchboard' @@ -116,7 +110,7 @@ def __init__( 'licensing restrictions. To enable flashing for this device type, ' 'install "esptool": "pip install esptool>=3.2".') - if chip_type not in esptool.SUPPORTED_CHIPS: # pytype: disable=module-attr + if chip_type not in esptool.CHIP_LIST: # pytype: disable=module-attr raise ValueError(f'Chip {chip_type} not supported by esptool.') super().__init__(device_name=device_name) diff --git a/gazoo_device/capabilities/interfaces/matter_controller_base.py b/gazoo_device/capabilities/interfaces/matter_controller_base.py index fc4c602..8eb10ea 100644 --- a/gazoo_device/capabilities/interfaces/matter_controller_base.py +++ b/gazoo_device/capabilities/interfaces/matter_controller_base.py @@ -56,20 +56,14 @@ def commission(self, node_id: int, setup_code: str, """ @abc.abstractmethod - def decommission(self, node_id: int) -> None: - """Forgets a commissioned device with the given node id. - - Args: - node_id: Assigned node id to decommission. - """ + def decommission(self) -> None: + """Forgets a commissioned device with the given node id.""" @abc.abstractmethod - def read(self, node_id: int, endpoint_id: int, cluster: str, - attribute: str) -> Any: + def read(self, endpoint_id: int, cluster: str, attribute: str) -> Any: """Reads a cluster's attribute for the given node id and endpoint. Args: - node_id: Node ID assigned to the commissioned end device. endpoint_id: Endpoint ID within the node to read attribute from. cluster: Name of the cluster to read the attribute value from. attribute: Name of the cluster attribute to read. @@ -79,12 +73,11 @@ def read(self, node_id: int, endpoint_id: int, cluster: str, """ @abc.abstractmethod - def write(self, node_id: int, endpoint_id: int, cluster: str, attribute: str, + def write(self, endpoint_id: int, cluster: str, attribute: str, value: Any) -> None: """Writes a cluster's attribute for the given node id and endpoint. Args: - node_id: Node ID assigned to the commissioned end device. endpoint_id: Endpoint ID within the node to write attribute to. cluster: Name of the cluster to write the attribute value to (e.g. onoff). attribute: Name of the cluster attribute to write (e.g. on-time). @@ -92,12 +85,11 @@ def write(self, node_id: int, endpoint_id: int, cluster: str, attribute: str, """ @abc.abstractmethod - def send(self, node_id: int, endpoint_id: int, cluster: str, command: str, + def send(self, endpoint_id: int, cluster: str, command: str, arguments: Sequence[Any]) -> None: """Sends a command to a device with the given node id and endpoint. Args: - node_id: Node ID assigned to the commissioned end device. endpoint_id: Endpoint ID within the node to read attribute from. cluster: Name of the cluster to send the command to (e.g. onoff). command: Name of the command to send (e.g. toggle). diff --git a/gazoo_device/capabilities/interfaces/matter_endpoints_base.py b/gazoo_device/capabilities/interfaces/matter_endpoints_base.py index a0000b1..f3afdd1 100644 --- a/gazoo_device/capabilities/interfaces/matter_endpoints_base.py +++ b/gazoo_device/capabilities/interfaces/matter_endpoints_base.py @@ -14,20 +14,252 @@ """Interface for Matter endpoint capability wrapper.""" import abc -from typing import Mapping, Type +from typing import Any, Collection, List, Mapping, Optional, Set, Type +from gazoo_device import decorators +from gazoo_device import errors +from gazoo_device import gdm_logger from gazoo_device.capabilities.interfaces import capability_base +from gazoo_device.capabilities.matter_clusters.interfaces import cluster_base from gazoo_device.capabilities.matter_endpoints.interfaces import endpoint_base +import immutabledict ROOT_NODE_ENDPOINT_ID = 0 +logger = gdm_logger.get_logger() + class MatterEndpointsBase(capability_base.CapabilityBase): """Capability wrapper for accessing the Matter endpoint instances.""" + def __init__(self, device_name: str, **cluster_kwargs: Any): + """Initializes an instance of MatterEndpoints capability. + + Args: + device_name: Name of the device instance the capability is attached to. + **cluster_kwargs: Keyword arguments for initializing PigweedRPC/ChipTool + based cluster capability. + """ + super().__init__(device_name=device_name) + self._cluster_kwargs = cluster_kwargs + + # Endpoint ID to endpoint instance mapping + self._endpoints = {} + + # The endpoint ID to endpoint class mapping. + self._endpoint_id_to_class = {} + + # The endpoint class to endpoint ID mapping. + self._endpoint_class_to_id = {} + + # The endpoint ID to set of cluster classes mapping. + self._endpoint_id_to_clusters = {} + + # The endpoint ID to device type ID mapping. + self._endpoint_id_to_device_type_id = {} + @abc.abstractmethod - def get(self, endpoint_id: int) -> endpoint_base.EndpointBase: - """Gets the specific endpoint instance by endpoint ID.""" + def get_supported_endpoint_ids(self) -> List[int]: + """Gets the list of supported endpoint ids on the device.""" + + @abc.abstractmethod + def get_endpoint_class(self, + endpoint_id: int) -> Type[endpoint_base.EndpointBase]: + """Gets the endpoint class by the given endpoint ID. + + Args: + endpoint_id: The given endpoint ID on the device. + + Returns: + The endpoint class module. The method returns None if the given endpoint + ID does not have device type. + """ @abc.abstractmethod + def get_supported_clusters( + self, endpoint_id: int) -> Set[Type[cluster_base.ClusterBase]]: + """Retrieves the supported clusters from the given endpoint ID. + + Args: + endpoint_id: The given endpoint ID on the device. + + Returns: + Set of supported cluster capability classes. + """ + + @decorators.DynamicProperty + def endpoint_id_to_class( + self) -> Mapping[int, Optional[Type[endpoint_base.EndpointBase]]]: + """Returns the endpoint_id_to_class mapping.""" + self._fetch_endpoints_and_clusters() + return immutabledict.immutabledict(self._endpoint_id_to_class) + + @decorators.DynamicProperty + def endpoint_class_to_id( + self) -> Mapping[Type[endpoint_base.EndpointBase], int]: + """Returns the endpoint_class_to_id mapping.""" + self._fetch_endpoints_and_clusters() + return immutabledict.immutabledict(self._endpoint_class_to_id) + + @decorators.DynamicProperty + def endpoint_id_to_clusters( + self) -> Mapping[int, Set[Type[cluster_base.ClusterBase]]]: + """Returns the endpoint ID to cluster classes mapping.""" + self._fetch_endpoints_and_clusters() + return immutabledict.immutabledict(self._endpoint_id_to_clusters) + + @decorators.DynamicProperty + def endpoint_id_to_device_type_id(self) -> Mapping[int, int]: + """Returns the endpoint ID to device type ID mapping.""" + self._fetch_endpoints_and_clusters() + return immutabledict.immutabledict(self._endpoint_id_to_device_type_id) + + def _fetch_endpoints_and_clusters(self): + """Retrieves the supported endpoints and clusters from descriptor cluster. + + The descriptor cluster should only be queried if it has not previously been + called or the reset method is called. + """ + if not self._endpoint_id_to_class: + for endpoint_id in self.get_supported_endpoint_ids(): + endpoint_cls = self.get_endpoint_class(endpoint_id) + self._endpoint_id_to_class[endpoint_id] = endpoint_cls + self._endpoint_id_to_device_type_id[ + endpoint_id] = endpoint_cls.DEVICE_TYPE_ID + # Ensuring we store the first endpoint ID handled by this class. + # This mapping will be used in get_endpoint_instance_by_class method + if endpoint_cls not in self._endpoint_class_to_id: + self._endpoint_class_to_id[endpoint_cls] = endpoint_id + self._endpoint_id_to_clusters[endpoint_id] = ( + self.get_supported_clusters(endpoint_id)) + + @decorators.CapabilityLogDecorator(logger) + def get(self, endpoint_id: int) -> endpoint_base.EndpointBase: + """Gets the specific endpoint instance by endpoint ID. + + Args: + endpoint_id: Endpoint ID on the device. + + Returns: + The endpoint class for the given endpoint ID. + + Raises: + DeviceError: The given endpoint ID does not exist on the device. Or the + endpoint class for the given endpoint ID is not implemented yet. + """ + if endpoint_id not in self.endpoint_id_to_class: + raise errors.DeviceError( + f"Endpoint ID {endpoint_id} on {self._device_name} does not exist.") + + if endpoint_id not in self._endpoints: + endpoint_class = self.endpoint_id_to_class[endpoint_id] + supported_clusters = self.endpoint_id_to_clusters[endpoint_id] + device_type_id = self.endpoint_id_to_device_type_id[endpoint_id] + + self._endpoints[endpoint_id] = endpoint_class( + device_name=self._device_name, + identifier=endpoint_id, + device_type_id=device_type_id, + supported_clusters=frozenset(supported_clusters), + **self._cluster_kwargs) + + return self._endpoints[endpoint_id] + + @decorators.CapabilityLogDecorator(logger) def list(self) -> Mapping[int, Type[endpoint_base.EndpointBase]]: - """Lists all supported endpoints.""" + """Returns a mapping of endpoint ID to the supported endpoint class.""" + return self.endpoint_id_to_class + + @decorators.CapabilityLogDecorator(logger) + def get_endpoint_instance_by_class( + self, endpoint_class: Type[endpoint_base.EndpointBase] + ) -> endpoint_base.EndpointBase: + """Gets the endpoint instance by the given endpoint class. + + Args: + endpoint_class: The given Matter endpoint class. + + Raises: + DeviceError: When the given endpoint class is not supported on the device. + + Returns: + The endpoint instance. + """ + if endpoint_class not in self.endpoint_class_to_id: + raise errors.DeviceError( + f"Class {endpoint_class} is not supported on {self._device_name}.") + endpoint_id = self.endpoint_class_to_id[endpoint_class] + return self.get(endpoint_id) + + @decorators.CapabilityLogDecorator(logger) + def reset(self) -> None: + """Resets the endpoint ID and endpoint class mappings.""" + self._endpoint_id_to_class.clear() + self._endpoint_class_to_id.clear() + self._endpoint_id_to_clusters.clear() + self._endpoint_id_to_device_type_id.clear() + self._endpoints.clear() + + @decorators.CapabilityLogDecorator(logger) + def has_endpoints(self, endpoint_names: Collection[str]) -> bool: + """Checks whether the device supports all the given endpoint names. + + Args: + endpoint_names: The collection of endpoint names. The names are + case-insensitive. Some valid examples are: "on_off_light", + "On_Off_Light". + + Raises: + ValueError when the given endpoint name is invalid or not supported in + GDM. + + Returns: + True if the device supports all the endpoints, false otherwise. + """ + valid_endpoint_name_to_class = { + endpoint_class.get_capability_name(): endpoint_class + for endpoint_class in self._SUPPORTED_ENDPOINTS + } + supported_endpoints = set(self.list().values()) + for endpoint_name in endpoint_names: + endpoint = valid_endpoint_name_to_class.get(endpoint_name.lower()) + if endpoint is None: + raise ValueError(f"Endpoint {endpoint_name} is not recognized. " + "Valid endpoints are: " + f"{list(valid_endpoint_name_to_class.keys())}") + if endpoint not in supported_endpoints: + return False + return True + + @decorators.CapabilityLogDecorator(logger) + def get_supported_endpoints(self) -> List[str]: + """Returns names of endpoints supported by the device.""" + return sorted(endpoint.get_capability_name() + for endpoint in self.get_supported_endpoint_flavors()) + + @decorators.CapabilityLogDecorator(logger) + def get_supported_endpoint_flavors( + self) -> List[Type[endpoint_base.EndpointBase]]: + """Returns flavors of endpoints supported by the device.""" + return [ + endpoint for endpoint in self.list().values() if endpoint is not None + ] + + @decorators.CapabilityLogDecorator(logger) + def get_supported_endpoints_and_clusters(self) -> Mapping[int, Set[str]]: + """Returns the supported endpoint IDs and set of cluster names mapping.""" + return { + endpoint_id: self.get(endpoint_id).get_supported_clusters() + for endpoint_id in self.list() + } + + @decorators.CapabilityLogDecorator(logger) + def get_supported_endpoint_instances_and_cluster_flavors( + self + ) -> Mapping[Type[endpoint_base.EndpointBase], + Set[Type[cluster_base.ClusterBase]]]: + """Returns the supported endpoint instance and cluster flavors mapping.""" + mapping = {} + for endpoint_id in self.list(): + endpoint = self.get(endpoint_id) + mapping[endpoint] = endpoint.get_supported_cluster_flavors() + return mapping diff --git a/gazoo_device/capabilities/matter_clusters/interfaces/cluster_base.py b/gazoo_device/capabilities/matter_clusters/interfaces/cluster_base.py index 6d834d1..c822416 100644 --- a/gazoo_device/capabilities/matter_clusters/interfaces/cluster_base.py +++ b/gazoo_device/capabilities/matter_clusters/interfaces/cluster_base.py @@ -13,6 +13,7 @@ # limitations under the License. """Interface for the Matter cluster capability.""" +import inspect from typing import Any, Callable, Optional from gazoo_device.capabilities.interfaces import capability_base @@ -43,3 +44,20 @@ def __init__(self, self._read = read self._write = write self._send = send + + def __setattr__(self, name: str, value: Any) -> None: + """Overrides the __setattr__ to check if setting to the valid attribute.""" + def is_setter_property(member): + return isinstance(member, property) and member.fset is not None + + valid_attributes = { + name for name, _ in inspect.getmembers( + self.__class__, is_setter_property)} + + # Skip the private attribute: _device_name, _healthy, _endpoint_id etc. + if name not in valid_attributes and not name.startswith("_"): + raise AttributeError( + f"Invalid attribute '{name}' to set. Valid attributes are " + f"{valid_attributes}.") + + super().__setattr__(name, value) diff --git a/gazoo_device/capabilities/matter_clusters/pressure_measurement_pw_rpc.py b/gazoo_device/capabilities/matter_clusters/pressure_measurement_pw_rpc.py index 3768e1d..9e654b5 100644 --- a/gazoo_device/capabilities/matter_clusters/pressure_measurement_pw_rpc.py +++ b/gazoo_device/capabilities/matter_clusters/pressure_measurement_pw_rpc.py @@ -15,6 +15,7 @@ """Pigweed RPC implementation of Matter Pressure Measurement cluster capability. """ from gazoo_device import decorators +from gazoo_device import errors from gazoo_device.capabilities import matter_enums from gazoo_device.capabilities.matter_clusters.interfaces import pressure_measurement_base from gazoo_device.protos import attributes_service_pb2 @@ -34,12 +35,15 @@ def measured_value(self) -> int: Returns: The MeasuredValue attribute. """ - measured_value_data = self._read( - endpoint_id=self._endpoint_id, - cluster_id=_PressureMeasurementCluster.ID, + return self._read_value( + attribute_id=_PressureMeasurementCluster.ATTRIBUTE_MEASURED_VALUE) + + @measured_value.setter + def measured_value(self, value: int) -> None: + """Updates the MeasuredValue attribute with new value.""" + self._write_value( attribute_id=_PressureMeasurementCluster.ATTRIBUTE_MEASURED_VALUE, - attribute_type=INT16_ATTRIBUTE_TYPE) - return measured_value_data.data_int16 + value=value) @decorators.DynamicProperty def min_measured_value(self) -> int: @@ -51,12 +55,15 @@ def min_measured_value(self) -> int: Returns: The MinMeasuredValue attribute. """ - min_measured_value_data = self._read( - endpoint_id=self._endpoint_id, - cluster_id=_PressureMeasurementCluster.ID, + return self._read_value( + attribute_id=_PressureMeasurementCluster.ATTRIBUTE_MIN_MEASURED_VALUE) + + @min_measured_value.setter + def min_measured_value(self, value: int) -> None: + """Updates the MinMeasuredValue attribute with new value.""" + self._write_value( attribute_id=_PressureMeasurementCluster.ATTRIBUTE_MIN_MEASURED_VALUE, - attribute_type=INT16_ATTRIBUTE_TYPE) - return min_measured_value_data.data_int16 + value=value) @decorators.DynamicProperty def max_measured_value(self) -> int: @@ -68,9 +75,49 @@ def max_measured_value(self) -> int: Returns: The MaxMeasuredValue attribute. """ - max_measured_value_data = self._read( + return self._read_value( + attribute_id=_PressureMeasurementCluster.ATTRIBUTE_MAX_MEASURED_VALUE) + + @max_measured_value.setter + def max_measured_value(self, value: int) -> None: + """Updates the MaxMeasuredValue attribute with new value.""" + self._write_value( + attribute_id=_PressureMeasurementCluster.ATTRIBUTE_MAX_MEASURED_VALUE, + value=value) + + def _read_value(self, attribute_id: int) -> int: + """Reads the value from the given attribute ID. + + Args: + attribute_id: Attribute ID on PressureMeasurementCluster. + + Returns: + The value read from the attribute. + """ + value_data = self._read( endpoint_id=self._endpoint_id, cluster_id=_PressureMeasurementCluster.ID, - attribute_id=_PressureMeasurementCluster.ATTRIBUTE_MAX_MEASURED_VALUE, + attribute_id=attribute_id, attribute_type=INT16_ATTRIBUTE_TYPE) - return max_measured_value_data.data_int16 + return value_data.data_int16 + + def _write_value(self, attribute_id: int, value: int) -> None: + """Writes the value to the given attribute ID. + + Args: + attribute_id: Attribute ID on PressureMeasurementCluster. + value: Value to write to the attribute. + + Raises: + DeviceError when the attribute value doesn't change. + """ + self._write( + endpoint_id=self._endpoint_id, + cluster_id=_PressureMeasurementCluster.ID, + attribute_id=attribute_id, + attribute_type=INT16_ATTRIBUTE_TYPE, + data_int16=value) + if self._read_value(attribute_id) != value: + raise errors.DeviceError( + f"Device {self._device_name} Attribute {attribute_id} didn't change" + f" to {value}") diff --git a/gazoo_device/capabilities/matter_clusters/temperature_measurement_pw_rpc.py b/gazoo_device/capabilities/matter_clusters/temperature_measurement_pw_rpc.py index 1ddcbad..e7e12fd 100644 --- a/gazoo_device/capabilities/matter_clusters/temperature_measurement_pw_rpc.py +++ b/gazoo_device/capabilities/matter_clusters/temperature_measurement_pw_rpc.py @@ -15,6 +15,7 @@ """RPC implementation of Matter Temperature Measurement cluster capability. """ from gazoo_device import decorators +from gazoo_device import errors from gazoo_device.capabilities import matter_enums from gazoo_device.capabilities.matter_clusters.interfaces import temperature_measurement_base from gazoo_device.protos import attributes_service_pb2 @@ -34,7 +35,7 @@ def _convert_attribute_constraint_value( the spec: MinMeasuredValue has range from -27315 to 32766; MaxMeasuredValue has range from -27314 to 32767. However, the returned value from the Ember API call will always be an unsigned value (which is always >= 0), therefore - we'll need to manually substract the 2^16 for complement. (ex: returned value + we'll need to manually subtract the 2^16 for complement. (ex: returned value = 38221 is actually 38221 - 2^16 = -27315 in spec) Args: @@ -64,12 +65,15 @@ def measured_value(self) -> int: Returns: The MeasuredValue attribute. """ - measured_value_data = self._read( - endpoint_id=self._endpoint_id, - cluster_id=_TempMeasurementCluster.ID, + return self._read_value( + attribute_id=_TempMeasurementCluster.ATTRIBUTE_MEASURED_VALUE) + + @measured_value.setter + def measured_value(self, value: int) -> None: + """Updates the MeasuredValue attribute with new value.""" + self._write_value( attribute_id=_TempMeasurementCluster.ATTRIBUTE_MEASURED_VALUE, - attribute_type=INT16_ATTRIBUTE_TYPE) - return measured_value_data.data_int16 + value=value) @decorators.DynamicProperty def min_measured_value(self) -> int: @@ -81,13 +85,17 @@ def min_measured_value(self) -> int: Returns: The MinMeasuredValue attribute. """ - min_measured_value_data = self._read( - endpoint_id=self._endpoint_id, - cluster_id=_TempMeasurementCluster.ID, - attribute_id=_TempMeasurementCluster.ATTRIBUTE_MIN_MEASURED_VALUE, - attribute_type=INT16_ATTRIBUTE_TYPE) + min_value = self._read_value( + attribute_id=_TempMeasurementCluster.ATTRIBUTE_MIN_MEASURED_VALUE) return _convert_attribute_constraint_value( - min_measured_value_data.data_int16, _MIN_MEASURED_UPPERBOUND) + min_value, _MIN_MEASURED_UPPERBOUND) + + @min_measured_value.setter + def min_measured_value(self, value: int) -> None: + """Updates the MinMeasuredValue attribute with new value.""" + self._write_value( + attribute_id=_TempMeasurementCluster.ATTRIBUTE_MIN_MEASURED_VALUE, + value=value) @decorators.DynamicProperty def max_measured_value(self) -> int: @@ -99,10 +107,36 @@ def max_measured_value(self) -> int: Returns: The MaxMeasuredValue attribute. """ - max_measured_value_data = self._read( + max_value = self._read_value( + attribute_id=_TempMeasurementCluster.ATTRIBUTE_MAX_MEASURED_VALUE) + return _convert_attribute_constraint_value( + max_value, _MAX_MEASURED_UPPERBOUND) + + @max_measured_value.setter + def max_measured_value(self, value: int) -> None: + """Updates the MaxMeasuredValue attribute with new value.""" + self._write_value( + attribute_id=_TempMeasurementCluster.ATTRIBUTE_MAX_MEASURED_VALUE, + value=value) + + def _read_value(self, attribute_id: int) -> int: + """Reads the value from the given attribute ID.""" + value_data = self._read( endpoint_id=self._endpoint_id, cluster_id=_TempMeasurementCluster.ID, - attribute_id=_TempMeasurementCluster.ATTRIBUTE_MAX_MEASURED_VALUE, + attribute_id=attribute_id, attribute_type=INT16_ATTRIBUTE_TYPE) - return _convert_attribute_constraint_value( - max_measured_value_data.data_int16, _MAX_MEASURED_UPPERBOUND) + return value_data.data_int16 + + def _write_value(self, attribute_id: int, value: int) -> None: + """Writes the value to the given attribute ID.""" + self._write( + endpoint_id=self._endpoint_id, + cluster_id=_TempMeasurementCluster.ID, + attribute_id=attribute_id, + attribute_type=INT16_ATTRIBUTE_TYPE, + data_int16=value) + if self._read_value(attribute_id) != value: + raise errors.DeviceError( + f"Device {self._device_name} Attribute {attribute_id} didn't change " + f"to {value}") diff --git a/gazoo_device/capabilities/matter_controller_chip_tool.py b/gazoo_device/capabilities/matter_controller_chip_tool.py index a39fc06..a7e9664 100644 --- a/gazoo_device/capabilities/matter_controller_chip_tool.py +++ b/gazoo_device/capabilities/matter_controller_chip_tool.py @@ -32,6 +32,7 @@ _CHIP_TOOL_BINARY_PATH = "/usr/local/bin/chip-tool" _HEX_PREFIX = "hex:" +_MATTER_NODE_ID_PROPERTY = "matter_node_id" _COMMANDS = immutabledict.immutabledict({ "READ_CLUSTER_ATTRIBUTE": @@ -116,6 +117,8 @@ def __init__(self, shell_fn: Callable[..., str], regex_shell_fn: Callable[..., str], send_file_to_device: Callable[[str, str], None], + set_property_fn: Callable[..., None], + get_property_fn: Callable[..., Any], chip_tool_path: str = _CHIP_TOOL_BINARY_PATH): """Creates an instance of MatterControllerChipTool capability. @@ -126,6 +129,8 @@ def __init__(self, instance. send_file_to_device: Bound 'send_file_to_device' method of the device's file transfer capability instance. + set_property_fn: Bound 'set_property' method of the Manager instance. + get_property_fn: Bound 'get_property' method of the Manager instance. chip_tool_path: Path to chip-tool binary on the device. """ super().__init__(device_name) @@ -134,12 +139,19 @@ def __init__(self, self._shell_with_regex = regex_shell_fn self._shell = shell_fn self._send_file_to_device = send_file_to_device + self._get_property_fn = get_property_fn + self._set_property_fn = set_property_fn @decorators.DynamicProperty def version(self) -> str: """Matter SDK version of the controller.""" return self._shell(_COMMANDS["CHIP_TOOL_VERSION"]) + @decorators.DynamicProperty + def path(self) -> str: + """Path to chip-tool binary.""" + return self._chip_tool_path + @decorators.CapabilityLogDecorator(logger) def commission(self, node_id: int, @@ -202,27 +214,25 @@ def commission(self, raise_error=True, timeout=_TIMEOUTS["COMMISSION"]) - @decorators.CapabilityLogDecorator(logger) - def decommission(self, node_id: int) -> None: - """Forgets a commissioned device with the given node id. + self._set_property_fn(self._device_name, _MATTER_NODE_ID_PROPERTY, node_id) - Args: - node_id: Assigned node id to decommission. - """ + @decorators.CapabilityLogDecorator(logger) + def decommission(self) -> None: + """Forgets a commissioned device with the given node id.""" command = _COMMANDS["DECOMMISSION"].format( - chip_tool=self._chip_tool_path, node_id=node_id) + chip_tool=self._chip_tool_path, + node_id=self._get_property_fn(_MATTER_NODE_ID_PROPERTY)) self._shell_with_regex( command, _REGEXES["DECOMMISSION_COMPLETE"], raise_error=True) + self._set_property_fn(self._device_name, _MATTER_NODE_ID_PROPERTY, None) - def read(self, node_id: int, endpoint_id: int, cluster: str, - attribute: str) -> Any: + def read(self, endpoint_id: int, cluster: str, attribute: str) -> Any: """Reads a cluster's attribute for the given node id and endpoint. Only primitive attribute values (integer, float, boolean and string) are supported. Args: - node_id: Node ID assigned to the commissioned end device. endpoint_id: Endpoint ID within the node to read attribute from. cluster: Name of the cluster to read the attribute value from. attribute: Name of the cluster attribute to read. @@ -232,7 +242,7 @@ def read(self, node_id: int, endpoint_id: int, cluster: str, """ command = _COMMANDS["READ_CLUSTER_ATTRIBUTE"].format( chip_tool=self._chip_tool_path, - node_id=node_id, + node_id=self._get_property_fn(_MATTER_NODE_ID_PROPERTY), endpoint_id=endpoint_id, cluster=cluster, attribute=attribute, @@ -248,12 +258,11 @@ def read(self, node_id: int, endpoint_id: int, cluster: str, return response @decorators.CapabilityLogDecorator(logger) - def write(self, node_id: int, endpoint_id: int, cluster: str, attribute: str, + def write(self, endpoint_id: int, cluster: str, attribute: str, value: Any) -> None: """Writes a cluster's attribute for the given node id and endpoint. Args: - node_id: Node ID assigned to the commissioned end device. endpoint_id: Endpoint ID within the node to write attribute to. cluster: Name of the cluster to write the attribute value to (e.g. onoff). attribute: Name of the cluster attribute to write (e.g. on-time). @@ -261,7 +270,7 @@ def write(self, node_id: int, endpoint_id: int, cluster: str, attribute: str, """ command = _COMMANDS["WRITE_CLUSTER_ATTRIBUTE"].format( chip_tool=self._chip_tool_path, - node_id=node_id, + node_id=self._get_property_fn(_MATTER_NODE_ID_PROPERTY), endpoint_id=endpoint_id, cluster=cluster, attribute=attribute, @@ -275,12 +284,11 @@ def write(self, node_id: int, endpoint_id: int, cluster: str, attribute: str, f"status code: {status_code}") @decorators.CapabilityLogDecorator(logger) - def send(self, node_id: int, endpoint_id: int, cluster: str, command: str, + def send(self, endpoint_id: int, cluster: str, command: str, arguments: Sequence[Any]) -> None: """Sends a command to a device with the given node id and endpoint. Args: - node_id: Node ID assigned to the commissioned end device. endpoint_id: Endpoint ID within the node to read attribute from. cluster: Name of the cluster to send the command to (e.g. onoff). command: Name of the command to send (e.g. toggle). @@ -288,7 +296,7 @@ def send(self, node_id: int, endpoint_id: int, cluster: str, command: str, """ command = _COMMANDS["SEND_CLUSTER_COMMMAND"].format( chip_tool=self._chip_tool_path, - node_id=node_id, + node_id=self._get_property_fn(_MATTER_NODE_ID_PROPERTY), endpoint_id=endpoint_id, cluster=cluster, command=command, diff --git a/gazoo_device/capabilities/matter_endpoints_accessor.py b/gazoo_device/capabilities/matter_endpoints_accessor.py deleted file mode 100644 index c90095f..0000000 --- a/gazoo_device/capabilities/matter_endpoints_accessor.py +++ /dev/null @@ -1,470 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Matter endpoint capability wrapper.""" -import copy -from typing import Any, Callable, Collection, List, Mapping, Optional, Set, Type - -from gazoo_device import decorators -from gazoo_device import errors -from gazoo_device import gdm_logger -from gazoo_device.capabilities import matter_endpoints_and_clusters -from gazoo_device.capabilities.interfaces import matter_endpoints_base -from gazoo_device.capabilities.matter_clusters.interfaces import cluster_base -from gazoo_device.capabilities.matter_endpoints import unsupported_endpoint -from gazoo_device.capabilities.matter_endpoints.interfaces import endpoint_base -from gazoo_device.protos import attributes_service_pb2 -from gazoo_device.protos import descriptor_service_pb2 -from gazoo_device.switchboard.transports import pigweed_rpc_transport -from gazoo_device.utility import pwrpc_utils - -_DESCRIPTOR_SERVICE_NAME = "Descriptor" -_DESCRIPTOR_GET_ENDPOINTS_RPC_NAME = "PartsList" -_DESCRIPTOR_DEVICE_TYPE_RPC_NAME = "DeviceTypeList" -_DESCRIPTOR_GET_CLUSTERS_RPC_NAME = "ServerList" -_ATTRIBUTE_SERVICE_NAME = "Attributes" -_ATTRIBUTE_READ_RPC_NAME = "Read" -_ATTRIBUTE_WRITE_RPC_NAME = "Write" -_ATTRIBUTE_DATA_MODULE_PATH = "gazoo_device.protos.attributes_service_pb2.AttributeData" -_ATTRIBUTE_METADATA_MODULE_PATH = "gazoo_device.protos.attributes_service_pb2.AttributeMetadata" - -logger = gdm_logger.get_logger() - - -class _DescriptorServiceHandler: - """Descriptor RPC service handler. - - Handler which leverages the descriptor RPC service on Matter device to get - the list of supported endpoint IDs and their device types. It allows the - implementation of generic Matter device controller on different platforms. - """ - - def __init__( - self, - device_name: str, - switchboard_call: Callable[..., Any], - rpc_timeout_s: int): - """Creates a descriptor service handler instance. - - Args: - device_name: Device name used for logging. - switchboard_call: The switchboard.call method. - rpc_timeout_s: Timeout (s) for RPC calls. - """ - self._switchboard_call = switchboard_call - self._rpc_timeout_s = rpc_timeout_s - self._device_name = device_name - - # The endpoint ID to endpoint class mapping. - self._endpoint_id_to_class = {} - - # The endpoint class to endpoint ID mapping. - self._endpoint_class_to_id = {} - - # The endpoint ID to set of cluster classes mapping. - self._endpoint_id_to_clusters = {} - - # The endpoint ID to device type ID mapping. - self._endpoint_id_to_device_type_id = {} - - @property - def endpoint_id_to_class( - self) -> Mapping[int, Optional[Type[endpoint_base.EndpointBase]]]: - """Returns the endpoint_id_to_class mapping.""" - self._fetch_endpoints_and_clusters() - return copy.deepcopy(self._endpoint_id_to_class) - - @property - def endpoint_class_to_id( - self) -> Mapping[Type[endpoint_base.EndpointBase], int]: - """Returns the endpoint_class_to_id mapping.""" - self._fetch_endpoints_and_clusters() - return copy.deepcopy(self._endpoint_class_to_id) - - @property - def endpoint_id_to_clusters( - self) -> Mapping[int, Set[Type[cluster_base.ClusterBase]]]: - """Returns the endpoint ID to cluster classes mapping.""" - self._fetch_endpoints_and_clusters() - return copy.deepcopy(self._endpoint_id_to_clusters) - - @property - def endpoint_id_to_device_type_id(self) -> Mapping[int, int]: - """Returns the endpoint ID to device type ID mapping.""" - self._fetch_endpoints_and_clusters() - return copy.deepcopy(self._endpoint_id_to_device_type_id) - - def reset(self) -> None: - """Resets the endpoint ID and endpoint class mapping.""" - self._endpoint_id_to_class.clear() - self._endpoint_class_to_id.clear() - self._endpoint_id_to_clusters.clear() - self._endpoint_id_to_device_type_id.clear() - - def _fetch_endpoints_and_clusters(self) -> None: - """Retrieves the supported endpoints from the descriptor RPC service. - - The set of supported clusters is also obtained by the descriptor RPC. Note - that RPC is only triggered when endpoint_id_to_class mapping is None. - """ - if not self._endpoint_id_to_class: - for endpoint_id in self._get_supported_endpoint_ids(): - endpoint_cls = self._get_endpoint_class(endpoint_id) - self._endpoint_id_to_class[endpoint_id] = endpoint_cls - # Ensuring we store the first endpoint ID handled by this class. - # This mapping will be used in get_endpoint_instance_by_class method - if endpoint_cls not in self._endpoint_class_to_id: - self._endpoint_class_to_id[endpoint_cls] = endpoint_id - self._endpoint_id_to_clusters[endpoint_id] = ( - self._get_supported_clusters(endpoint_id)) - - def _get_supported_endpoint_ids(self) -> List[int]: - """Gets the list of supported endpoint ids on the device.""" - ack, list_of_supported_endpoints = self._switchboard_call( - method=pigweed_rpc_transport.PigweedRPCTransport.rpc, - method_args=(_DESCRIPTOR_SERVICE_NAME, - _DESCRIPTOR_GET_ENDPOINTS_RPC_NAME), - method_kwargs={ - "endpoint": matter_endpoints_base.ROOT_NODE_ENDPOINT_ID, - "pw_rpc_timeout_s": self._rpc_timeout_s}) - if not ack: - raise errors.DeviceError( - f"Device {self._device_name} getting {_DESCRIPTOR_SERVICE_NAME} " - f"{_DESCRIPTOR_GET_ENDPOINTS_RPC_NAME} failed.") - supported_endpoint_ids = [] - for endpoint_in_bytes in list_of_supported_endpoints: - endpoint = descriptor_service_pb2.Endpoint.FromString(endpoint_in_bytes) - supported_endpoint_ids.append(endpoint.endpoint) - return supported_endpoint_ids - - def _get_endpoint_class( - self, endpoint_id: int) -> Type[endpoint_base.EndpointBase]: - """Gets the endpoint class by the given endpoint id. - - Args: - endpoint_id: The given endpoint ID on the device. - - Returns: - The endpoint class module. The method returns None if the given endpoint - ID does not have device type. - """ - ack, device_types = self._switchboard_call( - method=pigweed_rpc_transport.PigweedRPCTransport.rpc, - method_args=(_DESCRIPTOR_SERVICE_NAME, - _DESCRIPTOR_DEVICE_TYPE_RPC_NAME), - method_kwargs={ - "endpoint": endpoint_id, "pw_rpc_timeout_s": self._rpc_timeout_s}) - if not ack: - raise errors.DeviceError( - f"Device {self._device_name} getting {_DESCRIPTOR_SERVICE_NAME} " - f"{_DESCRIPTOR_DEVICE_TYPE_RPC_NAME} failed.") - device_type = descriptor_service_pb2.DeviceType.FromString(device_types[0]) - device_type_id = device_type.device_type - - # Store the endpoint ID to device type ID mapping. - self._endpoint_id_to_device_type_id[endpoint_id] = device_type_id - - return matter_endpoints_and_clusters.MATTER_DEVICE_TYPE_ID_TO_CLASS.get( - device_type_id, unsupported_endpoint.UnsupportedEndpoint) - - def _get_supported_clusters( - self, endpoint_id: int) -> Set[Type[cluster_base.ClusterBase]]: - """Retrieves the supported clusters from the given endpoint ID.""" - ack, clusters = self._switchboard_call( - method=pigweed_rpc_transport.PigweedRPCTransport.rpc, - method_args=(_DESCRIPTOR_SERVICE_NAME, - _DESCRIPTOR_GET_CLUSTERS_RPC_NAME), - method_kwargs={ - "endpoint": endpoint_id, "pw_rpc_timeout_s": self._rpc_timeout_s}) - if not ack: - raise errors.DeviceError( - f"Device {self._device_name} getting {_DESCRIPTOR_SERVICE_NAME} " - f"{_DESCRIPTOR_GET_CLUSTERS_RPC_NAME} failed.") - cluster_classes = set() - for cluster_in_bytes in clusters: - cluster = descriptor_service_pb2.Cluster.FromString(cluster_in_bytes) - cluster_class = matter_endpoints_and_clusters.CLUSTER_ID_TO_CLASS.get( - cluster.cluster_id) - if cluster_class is None: - logger.warning( - f"Cluster class for cluster ID {hex(cluster.cluster_id)} has not " - "been implemented yet.") - continue - cluster_classes.add(cluster_class) - return cluster_classes - - -class MatterEndpointsAccessor(matter_endpoints_base.MatterEndpointsBase): - """Capability wrapper for accessing the Matter endpoint instances.""" - - def __init__( - self, - device_name: str, - switchboard_call: Callable[..., Any], - rpc_timeout_s: int): - """Constructor of MatterEndpointsAccessor. - - Args: - device_name: Device name used for logging. - switchboard_call: The switchboard.call method. - rpc_timeout_s: Timeout (s) for RPC calls. - """ - super().__init__(device_name=device_name) - - # DescriptorServiceHandler instance for accessing endpoints and clusters - # information via Matter descriptor cluster. - self._descriptor_service_handler = _DescriptorServiceHandler( - device_name=device_name, - switchboard_call=switchboard_call, - rpc_timeout_s=rpc_timeout_s) - - # Endpoint ID to endpoint instance mapping - self._endpoints = {} - - # Arguments for Ember APIs. - self._switchboard_call = switchboard_call - self._rpc_timeout_s = rpc_timeout_s - - @decorators.CapabilityLogDecorator(logger) - def get(self, endpoint_id: int) -> endpoint_base.EndpointBase: - """Gets the specific endpoint instance by ID. - - Args: - endpoint_id: Endpoint ID on the device. - - Returns: - The endpoint class for the given endpoint ID. - - Raises: - DeviceError: The given endpoint ID does not exist on the device. Or the - endpoint class for the given endpoint ID is not implemented yet. - """ - if endpoint_id not in self._endpoints: - - if (endpoint_id not in - self._descriptor_service_handler.endpoint_id_to_class): - raise errors.DeviceError( - f"Endpoint ID {endpoint_id} on {self._device_name} does not exist.") - - # Obtain endpoint information from the descriptor handler. - endpoint_class = (self._descriptor_service_handler. - endpoint_id_to_class[endpoint_id]) - - supported_clusters = (self._descriptor_service_handler. - endpoint_id_to_clusters[endpoint_id]) - - device_type_id = (self._descriptor_service_handler. - endpoint_id_to_device_type_id[endpoint_id]) - - # Create endpoint instance. - self._endpoints[endpoint_id] = endpoint_class( - device_name=self._device_name, - identifier=endpoint_id, - device_type_id=device_type_id, - supported_clusters=frozenset(supported_clusters), - read=self.read, - write=self.write) - - return self._endpoints[endpoint_id] - - @decorators.CapabilityLogDecorator(logger) - def list(self) -> Mapping[int, Type[endpoint_base.EndpointBase]]: - """Returns a mapping of endpoint ID to the supported endpoint class.""" - return self._descriptor_service_handler.endpoint_id_to_class - - @decorators.CapabilityLogDecorator(logger) - def get_endpoint_instance_by_class( - self, endpoint_class: Type[endpoint_base.EndpointBase] - ) -> endpoint_base.EndpointBase: - """Gets the endpoint instance by the given endpoint class. - - Args: - endpoint_class: The given Matter endpoint class. - - Raises: - DeviceError: When the given endpoint class is not supported on the device. - - Returns: - The endpoint instance. - """ - if (endpoint_class not in - self._descriptor_service_handler.endpoint_class_to_id): - raise errors.DeviceError( - f"Class {endpoint_class} is not supported on {self._device_name}.") - endpoint_id = ( - self._descriptor_service_handler.endpoint_class_to_id[endpoint_class]) - return self.get(endpoint_id) - - @decorators.CapabilityLogDecorator(logger) - def reset(self) -> None: - """Resets the endpoint ID and endpoint class mapping.""" - self._descriptor_service_handler.reset() - - @decorators.CapabilityLogDecorator(logger) - def has_endpoints(self, endpoint_names: Collection[str]) -> bool: - """Checks whether the device supports all the given endpoint names. - - Args: - endpoint_names: The collection of endpoint names. The names are - case-insensitive. Some valid examples are: "on_off_light", - "On_Off_Light". - - Raises: - ValueError when the given endpoint name is invalid or not supported in - GDM. - - Returns: - True if the device supports all the endpoints, false otherwise. - """ - valid_endpoint_name_to_class = { - endpoint_class.get_capability_name(): endpoint_class - for endpoint_class in matter_endpoints_and_clusters.SUPPORTED_ENDPOINTS} - supported_endpoints = set(self.list().values()) - for endpoint_name in endpoint_names: - endpoint = valid_endpoint_name_to_class.get(endpoint_name.lower()) - if endpoint is None: - raise ValueError(f"Endpoint {endpoint_name} is not recognized. " - "Valid endpoints are: " - f"{list(valid_endpoint_name_to_class.keys())}") - if endpoint not in supported_endpoints: - return False - return True - - @decorators.CapabilityLogDecorator(logger) - def get_supported_endpoints(self) -> List[str]: - """Returns names of endpoints supported by the device.""" - return sorted(endpoint.get_capability_name() for endpoint in - self.get_supported_endpoint_flavors()) - - @decorators.CapabilityLogDecorator(logger) - def get_supported_endpoint_flavors( - self) -> List[Type[endpoint_base.EndpointBase]]: - """Returns flavors of endpoints supported by the device.""" - return [ - endpoint for endpoint in self.list().values() if endpoint is not None] - - @decorators.CapabilityLogDecorator(logger) - def get_supported_endpoints_and_clusters(self) -> Mapping[int, Set[str]]: - """Returns the supported endpoint IDs and set of cluster names mapping.""" - return { - endpoint_id: self.get(endpoint_id).get_supported_clusters() - for endpoint_id in self.list()} - - @decorators.CapabilityLogDecorator(logger) - def get_supported_endpoint_instances_and_cluster_flavors(self) -> Mapping[ - Type[endpoint_base.EndpointBase], Set[Type[cluster_base.ClusterBase]]]: - """Returns the supported endpoint instance and cluster flavors mapping.""" - mapping = {} - for endpoint_id in self.list(): - endpoint = self.get(endpoint_id) - mapping[endpoint] = endpoint.get_supported_cluster_flavors() - return mapping - - @decorators.CapabilityLogDecorator(logger) - def read( - self, - endpoint_id: int, - cluster_id: attributes_service_pb2.ClusterType, - attribute_id: int, - attribute_type: attributes_service_pb2.AttributeType - ) -> attributes_service_pb2.AttributeData: - """Ember API read method. - - Reads attribute data from the given endpoint ID, cluster ID and - attribute ID with the given attribute type. The endpoint ID is retrieved - via descriptor cluster. The attribute type is defined in the - attributes_service.proto, while cluster ID and attribute ID are defined in - the Matter spec. - - Args: - endpoint_id: Endpoint ID to read from. - cluster_id: Cluster ID to read from. - attribute_id: Attribute ID to read from. - attribute_type: Attribute data type to read. - - Returns: - Attribute data. - - Raises: - Device error when ack value is false. - """ - read_kwargs = { - "endpoint": endpoint_id, "cluster": cluster_id, - "attribute_id": attribute_id, "type": attribute_type, - "pw_rpc_timeout_s": self._rpc_timeout_s} - - ack, data_in_bytes = self._switchboard_call( - method=pigweed_rpc_transport.PigweedRPCTransport.rpc, - method_args=(_ATTRIBUTE_SERVICE_NAME, _ATTRIBUTE_READ_RPC_NAME), - method_kwargs=read_kwargs) - if not ack: - error_message = ( - f"Device {self._device_name} reading attribute (endpoint ID = " - f"{endpoint_id}, cluster ID = {cluster_id}, attribute ID = " - f"{attribute_id}) with attribute type {attribute_type} failed.") - raise errors.DeviceError(error_message) - - return attributes_service_pb2.AttributeData.FromString(data_in_bytes) - - @decorators.CapabilityLogDecorator(logger) - def write( - self, - endpoint_id: int, - cluster_id: attributes_service_pb2.ClusterType, - attribute_id: int, - attribute_type: attributes_service_pb2.AttributeType, - **data_kwargs: Any) -> None: - """Ember API write method. - - Write attribute data to the given endpoint ID, cluster ID and - attribute ID with the given attribute type. The endpoint ID is retrieved - via descriptor cluster. The attribute type is defined in the - attributes_service.proto, while cluster ID and attribute ID are defined in - the Matter spec. data_kwargs is the data we want to write to the device, the - supported data types are defined in AttributeData enum of - attributes_service.proto. - - Args: - endpoint_id: Endpoint ID to write to. - cluster_id: Cluster ID to write to. - attribute_id: Attribute ID to write to. - attribute_type: Attribute data type to write. - **data_kwargs: Attribute data to write. - - Raises: - Device error when ack value is false. - """ - data = attributes_service_pb2.AttributeData(**data_kwargs) - metadata = attributes_service_pb2.AttributeMetadata( - endpoint=endpoint_id, - cluster=cluster_id, - attribute_id=attribute_id, - type=attribute_type) - - serialized_data = pwrpc_utils.PigweedProtoState( - data, _ATTRIBUTE_DATA_MODULE_PATH) - serialized_metadata = pwrpc_utils.PigweedProtoState( - metadata, _ATTRIBUTE_METADATA_MODULE_PATH) - write_kwargs = {"data": serialized_data, "metadata": serialized_metadata} - - ack, _ = self._switchboard_call( - method=pigweed_rpc_transport.PigweedRPCTransport.rpc, - method_args=(_ATTRIBUTE_SERVICE_NAME, _ATTRIBUTE_WRITE_RPC_NAME), - method_kwargs=write_kwargs) - if not ack: - error_message = ( - f"Device {self._device_name} writing data: {data} to attribute (" - f"endpoint ID = {endpoint_id}, cluster ID = {cluster_id}, attribute " - f"ID = {attribute_id}) with attribute type {attribute_type} failed.") - raise errors.DeviceError(error_message) diff --git a/gazoo_device/capabilities/matter_endpoints_accessor_chip_tool.py b/gazoo_device/capabilities/matter_endpoints_accessor_chip_tool.py new file mode 100644 index 0000000..5a3ec3b --- /dev/null +++ b/gazoo_device/capabilities/matter_endpoints_accessor_chip_tool.py @@ -0,0 +1,134 @@ +"""chip-tool implementation of the Matter endpoints accessor capability. + +MatterEndpointsAccessorChipTool discovers the available endpoints on a Matter +device and exposes corresponding cluster capabilities, based on the results +retrieved via RaspberryPiMatterController using chip-tool binary. Communications +with the Matter end device are done via Matter protocol, and the end device is +expected to be commissioned by the same RaspberryPiMatterController. Since +chip-tool only supports commissioning a single device at a time, this capability +is attached to RaspberryPiMatterController instead of the Matter end device +for simplicity. +""" + +import re +from typing import Callable, List, Set, Type + +from gazoo_device import gdm_logger +from gazoo_device.capabilities import matter_endpoints_and_clusters +from gazoo_device.capabilities.interfaces import matter_controller_base +from gazoo_device.capabilities.interfaces import matter_endpoints_base +from gazoo_device.capabilities.matter_clusters.interfaces import cluster_base +from gazoo_device.capabilities.matter_endpoints import unsupported_endpoint +from gazoo_device.capabilities.matter_endpoints.interfaces import endpoint_base +import immutabledict + +logger = gdm_logger.get_logger() + +_COMMANDS = immutabledict.immutabledict({ + "READ_DESCRIPTOR_PARTS_LIST": + "{chip_tool} descriptor read parts-list {node_id} {endpoint_id}", + "READ_DESCRIPTOR_SERVER_LIST": + "{chip_tool} descriptor read server-list {node_id} {endpoint_id}", + "READ_DESCRIPTOR_DEVICE_LIST": + "{chip_tool} descriptor read device-list {node_id} {endpoint_id}", +}) + +_REGEXES = immutabledict.immutabledict({ + "DESCRIPTOR_ATTRIBUTE_RESPONSE": r"CHIP:DMG:\s+Data = (\w+)", + "DEVICE_LIST_RESPONSE": r"CHIP:TOO:\s+Type: (\d+)", +}) + + +class MatterEndpointsAccessorChipTool(matter_endpoints_base.MatterEndpointsBase + ): + """Capability for accessing the Matter endpoint instances via chip-tool.""" + + _SUPPORTED_ENDPOINTS = matter_endpoints_and_clusters.SUPPORTED_ENDPOINTS_CHIP_TOOL + + def __init__( + self, + device_name: str, + node_id_getter: Callable[[], int], + shell_fn: Callable[..., str], + shell_with_regex: Callable[..., str], + matter_controller: matter_controller_base.MatterControllerBase, + ) -> None: + """Initializes an instance of MatterEndpoints capability. + + Args: + device_name: Name of the device instance the capability is attached to. + node_id_getter: Getter method for Matter node ID of the commissioned + end device. + shell_fn: Bound 'shell' method of the device class instance. + shell_with_regex: Bound 'shell_with_regex' method of the device class + instance. + matter_controller: An instance of MatterController capability. + """ + super().__init__( + device_name=device_name, + read=matter_controller.read, + write=matter_controller.write, + send=matter_controller.send) + + self._matter_controller = matter_controller + self._node_id_getter = node_id_getter + self._shell_fn = shell_fn + self._shell_with_regex = shell_with_regex + + def get_supported_endpoint_ids(self) -> List[int]: + """Returns the list of supported endpoint ids on the device.""" + response = self._shell_fn(_COMMANDS["READ_DESCRIPTOR_PARTS_LIST"].format( + chip_tool=self._matter_controller.path, + endpoint_id=matter_endpoints_base.ROOT_NODE_ENDPOINT_ID, + node_id=self._node_id_getter())) + endpoints = re.findall(_REGEXES["DESCRIPTOR_ATTRIBUTE_RESPONSE"], response) + return [int(endpoint) for endpoint in endpoints] + + def get_endpoint_class(self, + endpoint_id: int) -> Type[endpoint_base.EndpointBase]: + """Gets the endpoint class by the given endpoint id. + + Args: + endpoint_id: The given endpoint ID on the device. + + Returns: + The endpoint class module, or UnsupportedEndpoint if the endpoint is not + yet supported in GDM. + """ + command = _COMMANDS["READ_DESCRIPTOR_DEVICE_LIST"].format( + chip_tool=self._matter_controller.path, + endpoint_id=endpoint_id, + node_id=self._node_id_getter()) + device_type_id = int( + self._shell_with_regex(command, _REGEXES["DEVICE_LIST_RESPONSE"])) + + return matter_endpoints_and_clusters.MATTER_DEVICE_TYPE_ID_TO_CLASS_CHIP_TOOL.get( + device_type_id, unsupported_endpoint.UnsupportedEndpoint) + + def get_supported_clusters( + self, endpoint_id: int) -> Set[Type[cluster_base.ClusterBase]]: + """Retrieves the supported clusters from the given endpoint ID. + + Args: + endpoint_id: The given endpoint ID on the device. + + Returns: + Set of supported cluster capability classes. + """ + response = self._shell_fn(_COMMANDS["READ_DESCRIPTOR_SERVER_LIST"].format( + chip_tool=self._matter_controller.path, + endpoint_id=endpoint_id, + node_id=self._node_id_getter())) + clusters = map( + int, re.findall(_REGEXES["DESCRIPTOR_ATTRIBUTE_RESPONSE"], response)) + + cluster_classes = [] + for cluster in clusters: + if cluster in matter_endpoints_and_clusters.CLUSTER_ID_TO_CLASS_CHIP_TOOL: + cluster_classes.append(matter_endpoints_and_clusters + .CLUSTER_ID_TO_CLASS_CHIP_TOOL[cluster]) + else: + logger.warning(f"Cluster class for cluster ID {hex(cluster)} has not " + "been implemented yet.") + + return set(cluster_classes) diff --git a/gazoo_device/capabilities/matter_endpoints_accessor_pw_rpc.py b/gazoo_device/capabilities/matter_endpoints_accessor_pw_rpc.py new file mode 100644 index 0000000..4930711 --- /dev/null +++ b/gazoo_device/capabilities/matter_endpoints_accessor_pw_rpc.py @@ -0,0 +1,233 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Matter endpoint capability wrapper via Pigweed RPC.""" +from typing import Any, Callable, List, Set, Type + +from gazoo_device import decorators +from gazoo_device import errors +from gazoo_device import gdm_logger +from gazoo_device.capabilities import matter_endpoints_and_clusters +from gazoo_device.capabilities.interfaces import matter_endpoints_base +from gazoo_device.capabilities.matter_clusters.interfaces import cluster_base +from gazoo_device.capabilities.matter_endpoints import unsupported_endpoint +from gazoo_device.capabilities.matter_endpoints.interfaces import endpoint_base +from gazoo_device.protos import attributes_service_pb2 +from gazoo_device.protos import descriptor_service_pb2 +from gazoo_device.switchboard.transports import pigweed_rpc_transport +from gazoo_device.utility import pwrpc_utils + +_DESCRIPTOR_SERVICE_NAME = "Descriptor" +_DESCRIPTOR_GET_ENDPOINTS_RPC_NAME = "PartsList" +_DESCRIPTOR_DEVICE_TYPE_RPC_NAME = "DeviceTypeList" +_DESCRIPTOR_GET_CLUSTERS_RPC_NAME = "ServerList" +_ATTRIBUTE_SERVICE_NAME = "Attributes" +_ATTRIBUTE_READ_RPC_NAME = "Read" +_ATTRIBUTE_WRITE_RPC_NAME = "Write" +_ATTRIBUTE_DATA_MODULE_PATH = "gazoo_device.protos.attributes_service_pb2.AttributeData" +_ATTRIBUTE_METADATA_MODULE_PATH = "gazoo_device.protos.attributes_service_pb2.AttributeMetadata" + +logger = gdm_logger.get_logger() + + +class MatterEndpointsAccessorPwRpc(matter_endpoints_base.MatterEndpointsBase): + """Capability wrapper for accessing the Matter endpoint instances.""" + + _SUPPORTED_ENDPOINTS = matter_endpoints_and_clusters.SUPPORTED_ENDPOINTS_PW_RPC + + def __init__(self, device_name: str, switchboard_call: Callable[..., Any], + rpc_timeout_s: int): + """Constructor of MatterEndpointsAccessorPwRpc. + + Args: + device_name: Device name used for logging. + switchboard_call: The switchboard.call method. + rpc_timeout_s: Timeout (s) for RPC calls. + """ + super().__init__(device_name=device_name, read=self.read, write=self.write) + self._switchboard_call = switchboard_call + self._rpc_timeout_s = rpc_timeout_s + + def get_supported_endpoint_ids(self) -> List[int]: + """Gets the list of supported endpoint ids on the device.""" + ack, list_of_supported_endpoints = self._switchboard_call( + method=pigweed_rpc_transport.PigweedRPCTransport.rpc, + method_args=(_DESCRIPTOR_SERVICE_NAME, + _DESCRIPTOR_GET_ENDPOINTS_RPC_NAME), + method_kwargs={ + "endpoint": matter_endpoints_base.ROOT_NODE_ENDPOINT_ID, + "pw_rpc_timeout_s": self._rpc_timeout_s}) + if not ack: + raise errors.DeviceError( + f"Device {self._device_name} getting {_DESCRIPTOR_SERVICE_NAME} " + f"{_DESCRIPTOR_GET_ENDPOINTS_RPC_NAME} failed.") + supported_endpoint_ids = [] + for endpoint_in_bytes in list_of_supported_endpoints: + endpoint = descriptor_service_pb2.Endpoint.FromString(endpoint_in_bytes) + supported_endpoint_ids.append(endpoint.endpoint) + return supported_endpoint_ids + + def get_endpoint_class(self, + endpoint_id: int) -> Type[endpoint_base.EndpointBase]: + """Gets the endpoint class by the given endpoint id. + + Args: + endpoint_id: The given endpoint ID on the device. + + Returns: + The endpoint class module, or UnsupportedEndpoint if the endpoint is not + yet supported in GDM. + """ + ack, device_types = self._switchboard_call( + method=pigweed_rpc_transport.PigweedRPCTransport.rpc, + method_args=(_DESCRIPTOR_SERVICE_NAME, + _DESCRIPTOR_DEVICE_TYPE_RPC_NAME), + method_kwargs={ + "endpoint": endpoint_id, "pw_rpc_timeout_s": self._rpc_timeout_s}) + if not ack: + raise errors.DeviceError( + f"Device {self._device_name} getting {_DESCRIPTOR_SERVICE_NAME} " + f"{_DESCRIPTOR_DEVICE_TYPE_RPC_NAME} failed.") + device_type = descriptor_service_pb2.DeviceType.FromString(device_types[0]) + device_type_id = device_type.device_type + + # Store the endpoint ID to device type ID mapping. + self._endpoint_id_to_device_type_id[endpoint_id] = device_type_id + + return matter_endpoints_and_clusters.MATTER_DEVICE_TYPE_ID_TO_CLASS_PW_RPC.get( + device_type_id, unsupported_endpoint.UnsupportedEndpoint) + + def get_supported_clusters( + self, endpoint_id: int) -> Set[Type[cluster_base.ClusterBase]]: + """Retrieves the supported clusters from the given endpoint ID.""" + ack, clusters = self._switchboard_call( + method=pigweed_rpc_transport.PigweedRPCTransport.rpc, + method_args=(_DESCRIPTOR_SERVICE_NAME, + _DESCRIPTOR_GET_CLUSTERS_RPC_NAME), + method_kwargs={ + "endpoint": endpoint_id, "pw_rpc_timeout_s": self._rpc_timeout_s}) + if not ack: + raise errors.DeviceError( + f"Device {self._device_name} getting {_DESCRIPTOR_SERVICE_NAME} " + f"{_DESCRIPTOR_GET_CLUSTERS_RPC_NAME} failed.") + cluster_classes = set() + for cluster_in_bytes in clusters: + cluster = descriptor_service_pb2.Cluster.FromString(cluster_in_bytes) + cluster_class = matter_endpoints_and_clusters.CLUSTER_ID_TO_CLASS_PW_RPC.get( + cluster.cluster_id) + if cluster_class is None: + logger.warning( + f"Cluster class for cluster ID {hex(cluster.cluster_id)} has not " + "been implemented yet.") + continue + cluster_classes.add(cluster_class) + return cluster_classes + + @decorators.CapabilityLogDecorator(logger) + def read( + self, + endpoint_id: int, + cluster_id: attributes_service_pb2.ClusterType, + attribute_id: int, + attribute_type: attributes_service_pb2.AttributeType + ) -> attributes_service_pb2.AttributeData: + """Ember API read method. + + Reads attribute data from the given endpoint ID, cluster ID and + attribute ID with the given attribute type. The endpoint ID is retrieved + via descriptor cluster. The attribute type is defined in the + attributes_service.proto, while cluster ID and attribute ID are defined in + the Matter spec. + + Args: + endpoint_id: Endpoint ID to read from. + cluster_id: Cluster ID to read from. + attribute_id: Attribute ID to read from. + attribute_type: Attribute data type to read. + + Returns: + Attribute data. + + Raises: + Device error when ack value is false. + """ + read_kwargs = { + "endpoint": endpoint_id, "cluster": cluster_id, + "attribute_id": attribute_id, "type": attribute_type, + "pw_rpc_timeout_s": self._rpc_timeout_s} + + ack, data_in_bytes = self._switchboard_call( + method=pigweed_rpc_transport.PigweedRPCTransport.rpc, + method_args=(_ATTRIBUTE_SERVICE_NAME, _ATTRIBUTE_READ_RPC_NAME), + method_kwargs=read_kwargs) + if not ack: + error_message = ( + f"Device {self._device_name} reading attribute (endpoint ID = " + f"{endpoint_id}, cluster ID = {cluster_id}, attribute ID = " + f"{attribute_id}) with attribute type {attribute_type} failed.") + raise errors.DeviceError(error_message) + + return attributes_service_pb2.AttributeData.FromString(data_in_bytes) + + @decorators.CapabilityLogDecorator(logger) + def write( + self, + endpoint_id: int, + cluster_id: attributes_service_pb2.ClusterType, + attribute_id: int, + attribute_type: attributes_service_pb2.AttributeType, + **data_kwargs: Any) -> None: + """Ember API write method. + + Write attribute data to the given endpoint ID, cluster ID and + attribute ID with the given attribute type. The endpoint ID is retrieved + via descriptor cluster. The attribute type is defined in the + attributes_service.proto, while cluster ID and attribute ID are defined in + the Matter spec. data_kwargs is the data we want to write to the device, the + supported data types are defined in AttributeData enum of + attributes_service.proto. + + Args: + endpoint_id: Endpoint ID to write to. + cluster_id: Cluster ID to write to. + attribute_id: Attribute ID to write to. + attribute_type: Attribute data type to write. + **data_kwargs: Attribute data to write. + + Raises: + Device error when ack value is false. + """ + data = attributes_service_pb2.AttributeData(**data_kwargs) + metadata = attributes_service_pb2.AttributeMetadata( + endpoint=endpoint_id, + cluster=cluster_id, + attribute_id=attribute_id, + type=attribute_type) + + serialized_data = pwrpc_utils.PigweedProtoState( + data, _ATTRIBUTE_DATA_MODULE_PATH) + serialized_metadata = pwrpc_utils.PigweedProtoState( + metadata, _ATTRIBUTE_METADATA_MODULE_PATH) + write_kwargs = {"data": serialized_data, "metadata": serialized_metadata} + + ack, _ = self._switchboard_call( + method=pigweed_rpc_transport.PigweedRPCTransport.rpc, + method_args=(_ATTRIBUTE_SERVICE_NAME, _ATTRIBUTE_WRITE_RPC_NAME), + method_kwargs=write_kwargs) + if not ack: + error_message = ( + f"Device {self._device_name} writing data: {data} to attribute (" + f"endpoint ID = {endpoint_id}, cluster ID = {cluster_id}, attribute " + f"ID = {attribute_id}) with attribute type {attribute_type} failed.") + raise errors.DeviceError(error_message) diff --git a/gazoo_device/capabilities/matter_endpoints_and_clusters.py b/gazoo_device/capabilities/matter_endpoints_and_clusters.py index 5bcff16..8be6fdc 100644 --- a/gazoo_device/capabilities/matter_endpoints_and_clusters.py +++ b/gazoo_device/capabilities/matter_endpoints_and_clusters.py @@ -18,6 +18,7 @@ from gazoo_device.capabilities.matter_clusters import door_lock_pw_rpc from gazoo_device.capabilities.matter_clusters import level_control_pw_rpc from gazoo_device.capabilities.matter_clusters import occupancy_pw_rpc +from gazoo_device.capabilities.matter_clusters import on_off_chip_tool from gazoo_device.capabilities.matter_clusters import on_off_pw_rpc from gazoo_device.capabilities.matter_clusters import pressure_measurement_pw_rpc from gazoo_device.capabilities.matter_clusters import temperature_measurement_pw_rpc @@ -30,7 +31,7 @@ import immutabledict -SUPPORTED_ENDPOINTS = ( +SUPPORTED_ENDPOINTS_PW_RPC = ( color_temperature_light.ColorTemperatureLightEndpoint, dimmable_light.DimmableLightEndpoint, door_lock.DoorLockEndpoint, @@ -38,7 +39,7 @@ pressure_sensor.PressureSensorEndpoint, temperature_sensor.TemperatureSensorEndpoint) -SUPPORTED_CLUSTERS = ( +SUPPORTED_CLUSTERS_PW_RPC = ( color_control_pw_rpc.ColorControlClusterPwRpc, door_lock_pw_rpc.DoorLockClusterPwRpc, level_control_pw_rpc.LevelControlClusterPwRpc, @@ -47,12 +48,26 @@ pressure_measurement_pw_rpc.PressureMeasurementClusterPwRpc, temperature_measurement_pw_rpc.TemperatureMeasurementClusterPwRpc) -MATTER_DEVICE_TYPE_ID_TO_CLASS = immutabledict.immutabledict({ +MATTER_DEVICE_TYPE_ID_TO_CLASS_PW_RPC = immutabledict.immutabledict({ endpoint_class.DEVICE_TYPE_ID: endpoint_class - for endpoint_class in SUPPORTED_ENDPOINTS + for endpoint_class in SUPPORTED_ENDPOINTS_PW_RPC }) -CLUSTER_ID_TO_CLASS = immutabledict.immutabledict({ +CLUSTER_ID_TO_CLASS_PW_RPC = immutabledict.immutabledict({ cluster_class.CLUSTER_ID: cluster_class - for cluster_class in SUPPORTED_CLUSTERS + for cluster_class in SUPPORTED_CLUSTERS_PW_RPC +}) + +SUPPORTED_ENDPOINTS_CHIP_TOOL = (on_off_light.OnOffLightEndpoint,) + +SUPPORTED_CLUSTERS_CHIP_TOOL = (on_off_chip_tool.OnOffClusterChipTool,) + +MATTER_DEVICE_TYPE_ID_TO_CLASS_CHIP_TOOL = immutabledict.immutabledict({ + endpoint_class.DEVICE_TYPE_ID: endpoint_class + for endpoint_class in SUPPORTED_ENDPOINTS_CHIP_TOOL +}) + +CLUSTER_ID_TO_CLASS_CHIP_TOOL = immutabledict.immutabledict({ + cluster_class.CLUSTER_ID: cluster_class + for cluster_class in SUPPORTED_CLUSTERS_CHIP_TOOL }) diff --git a/gazoo_device/gazoo_device_controllers.py b/gazoo_device/gazoo_device_controllers.py index 7e38388..19e2172 100644 --- a/gazoo_device/gazoo_device_controllers.py +++ b/gazoo_device/gazoo_device_controllers.py @@ -43,7 +43,8 @@ from gazoo_device.capabilities import flash_build_jlink from gazoo_device.capabilities import led_driver_default from gazoo_device.capabilities import matter_controller_chip_tool -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_chip_tool +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities import package_management_android from gazoo_device.capabilities import pwrpc_button_default from gazoo_device.capabilities import pwrpc_common_default @@ -226,7 +227,8 @@ def export_extensions() -> Dict[str, Any]: led_driver_default.LedDriverDefault, level_control_pw_rpc.LevelControlClusterPwRpc, matter_controller_chip_tool.MatterControllerChipTool, - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_chip_tool.MatterEndpointsAccessorChipTool, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, occupancy_pw_rpc.OccupancyClusterPwRpc, on_off_light.OnOffLightEndpoint, on_off_chip_tool.OnOffClusterChipTool, diff --git a/gazoo_device/tests/functional_test_runner.py b/gazoo_device/tests/functional_test_runner.py index 4311f09..7de32bc 100644 --- a/gazoo_device/tests/functional_test_runner.py +++ b/gazoo_device/tests/functional_test_runner.py @@ -31,6 +31,7 @@ from gazoo_device.tests.functional_tests import door_lock_test_suite from gazoo_device.tests.functional_tests import embedded_script_test_suite from gazoo_device.tests.functional_tests import file_transfer_test_suite +from gazoo_device.tests.functional_tests import matter_endpoints_test_suite from gazoo_device.tests.functional_tests import on_off_light_test_suite from gazoo_device.tests.functional_tests import optional_properties_test_suite from gazoo_device.tests.functional_tests import package_management_test_suite @@ -65,6 +66,7 @@ file_transfer_test_suite.FileTransferTestSuite, dimmable_light_test_suite.DimmableLightTestSuite, door_lock_test_suite.DoorLockTestSuite, + matter_endpoints_test_suite.MatterEndpointsPwRpcTestSuite, on_off_light_test_suite.OnOffLightTestSuite, optional_properties_test_suite.OptionalPropertiesTestSuite, package_management_test_suite.PackageManagementTestSuite, diff --git a/gazoo_device/tests/functional_tests/matter_endpoints_test_suite.py b/gazoo_device/tests/functional_tests/matter_endpoints_test_suite.py new file mode 100644 index 0000000..985932b --- /dev/null +++ b/gazoo_device/tests/functional_tests/matter_endpoints_test_suite.py @@ -0,0 +1,85 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test suite for devices using the matter_endpoints capability.""" +from typing import Type +from gazoo_device.tests.functional_tests.utils import gdm_test_base +from mobly import asserts + + +class MatterEndpointsPwRpcTestSuite(gdm_test_base.GDMTestBase): + """Tests for the matter_endpoints capability.""" + + @classmethod + def is_applicable_to(cls, device_type: str, + device_class: Type[gdm_test_base.DeviceType], + device_name: str) -> bool: + """Determines if this test suite can run on the given device.""" + return device_class.has_capabilities(["matter_endpoints"]) + + @classmethod + def requires_pairing(cls) -> bool: + """Returns True if the device must be paired to run this test suite.""" + return False + + def test_list_method(self): + """Tests if list method returns non-empty mapping.""" + asserts.assert_true(bool(self.device.matter_endpoints.list()), + "The returned endpoint list should not be empty.") + + def test_get_method(self): + """Tests get method on success.""" + endpoint_ids = self.device.matter_endpoints.get_supported_endpoint_ids() + asserts.assert_true(bool(endpoint_ids), + "The supported endpoint IDs should not be empty.") + + # Every endpoint ID works, so pick the first one. + endpoint = self.device.matter_endpoints.get(endpoint_ids[0]) + asserts.assert_is_instance(endpoint.device_type_id, int) + + def test_get_supported_endpoints(self): + """Tests get_supported_endpoints on success.""" + endpoints = self.device.matter_endpoints.get_supported_endpoints() + asserts.assert_true(bool(endpoints), + "get_supported_endpoints should not return empty list.") + + def test_get_supported_endpoint_flavors(self): + """Tests get_supported_endpoint_flavors on success.""" + endpoint_flavors = ( + self.device.matter_endpoints.get_supported_endpoint_flavors()) + asserts.assert_true( + bool(endpoint_flavors), + "get_supported_endpoint_flavors should not return empty list.") + + def test_get_supported_endpoints_and_clusters(self): + """Tests get_supported_endpoints_and_clusters on success.""" + endpoints_and_clusters = ( + self.device.matter_endpoints.get_supported_endpoints_and_clusters()) + asserts.assert_true( + bool(endpoints_and_clusters), + "get_supported_endpoints_and_clusters should not return empty mapping.") + + def test_get_supported_endpoint_instances_and_cluster_flavors(self): + """Tests get_supported_endpoint_instances_and_cluster_flavors on success.""" + endpoint_instances_and_cluster_flavors = ( + self.device.matter_endpoints. + get_supported_endpoint_instances_and_cluster_flavors()) + asserts.assert_true( + bool(endpoint_instances_and_cluster_flavors), + "get_supported_endpoint_instances_and_cluster_flavors should not " + "return empty mapping.") + + +if __name__ == "__main__": + gdm_test_base.main() diff --git a/gazoo_device/tests/unit_tests/capability_tests/flash_build_esptool_test.py b/gazoo_device/tests/unit_tests/capability_tests/flash_build_esptool_test.py index 6b2f187..4121c89 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/flash_build_esptool_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/flash_build_esptool_test.py @@ -15,10 +15,10 @@ import os from unittest import mock -from esptool import esptool +import esptool from gazoo_device import errors from gazoo_device.capabilities import flash_build_esptool -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.tests.unit_tests.utils import fake_device_test_case _MOCK_DEVICE_NAME = 'MOCK_DEVICE' @@ -94,8 +94,8 @@ def setUp(self): serial_port=_MOCK_PORT, switchboard=self.mock_switchboard, baud=_MOCK_BAUDRATE, - reset_endpoints_fn=mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.reset)) + reset_endpoints_fn=mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.reset)) @mock.patch.object( flash_build_esptool.FlashBuildEsptool, '_verify_file', autospec=True) diff --git a/gazoo_device/tests/unit_tests/capability_tests/flash_build_jlink_test.py b/gazoo_device/tests/unit_tests/capability_tests/flash_build_jlink_test.py index 60a5a2f..eba4d56 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/flash_build_jlink_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/flash_build_jlink_test.py @@ -18,7 +18,7 @@ from gazoo_device import config from gazoo_device import errors from gazoo_device.capabilities import flash_build_jlink -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.switchboard import switchboard from gazoo_device.tests.unit_tests.utils import fake_device_test_case from gazoo_device.utility import retry @@ -45,7 +45,8 @@ def setUp(self): self.addCleanup(jlink_patcher.stop) self.mock_jlink = mock_jlink_class.return_value self.mock_matter_endpoints_reset = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.reset) + spec=matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc.reset + ) self.uut = flash_build_jlink.FlashBuildJLink( device_name=_MOCK_DEVICE_NAME, serial_number=_MOCK_SERIAL_NUMBER, diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/cluster_base_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/cluster_base_test.py index 5efddee..afe021d 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/cluster_base_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/cluster_base_test.py @@ -13,12 +13,12 @@ # limitations under the License. """Matter cluster capability unit test for cluster_base module.""" - from gazoo_device.capabilities.matter_clusters.interfaces import cluster_base from gazoo_device.tests.unit_tests.utils import fake_device_test_case _FAKE_DEVICE_NAME = "fake-device-name" _FAKE_ENDPOINT_ID = 1 +_FAKE_ATTRIBUTE = "fake_attribute" class ClusterBaseTest(fake_device_test_case.FakeDeviceTestCase): @@ -32,10 +32,14 @@ def setUp(self): read=None, write=None) - def test_cluster_initialization(self): - """Verifies cluster base is initialized successfully.""" - # TODO(b/206894490) Remove this test once the other unit tests are added. - self.assertIsNotNone(self.uut) + def test_setattr_method_on_failure(self): + """Verifies the overridden __setattr__ on failure. + + The on success scenarios are covered by the other cluster unit tests. + """ + with self.assertRaisesRegex( + AttributeError, f"Invalid attribute '{_FAKE_ATTRIBUTE}' to set"): + self.uut.fake_attribute = 0 if __name__ == "__main__": diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/color_control_pw_rpc_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/color_control_pw_rpc_test.py index a69d502..3a83807 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/color_control_pw_rpc_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/color_control_pw_rpc_test.py @@ -17,7 +17,7 @@ import gazoo_device from gazoo_device import errors -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities.matter_clusters import color_control_pw_rpc from gazoo_device.tests.unit_tests.utils import fake_device_test_case @@ -35,10 +35,10 @@ class ColorControlClusterPwRpcTest(fake_device_test_case.FakeDeviceTestCase): def setUp(self): super().setUp() - self.fake_read = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.read) - self.fake_write = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.write) + self.fake_read = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.read) + self.fake_write = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.write) self.uut = color_control_pw_rpc.ColorControlClusterPwRpc( device_name=_FAKE_DEVICE_NAME, endpoint_id=_FAKE_ENDPOINT_ID, diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/door_lock_pw_rpc_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/door_lock_pw_rpc_test.py index 6ae2162..a319e45 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/door_lock_pw_rpc_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/door_lock_pw_rpc_test.py @@ -18,7 +18,7 @@ from absl.testing import parameterized import gazoo_device from gazoo_device import errors -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities import matter_enums from gazoo_device.capabilities.matter_clusters import door_lock_pw_rpc from gazoo_device.tests.unit_tests.utils import fake_device_test_case @@ -35,10 +35,10 @@ class DoorLockClusterPwRpcTest(fake_device_test_case.FakeDeviceTestCase): def setUp(self): super().setUp() - self.fake_read = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.read) - self.fake_write = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.write) + self.fake_read = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.read) + self.fake_write = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.write) self.uut = door_lock_pw_rpc.DoorLockClusterPwRpc( device_name=_FAKE_DEVICE_NAME, endpoint_id=_FAKE_ENDPOINT_ID, diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/level_control_pw_rpc_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/level_control_pw_rpc_test.py index 54a1f2d..116c203 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/level_control_pw_rpc_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/level_control_pw_rpc_test.py @@ -17,7 +17,7 @@ import gazoo_device from gazoo_device import errors -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities.matter_clusters import level_control_pw_rpc from gazoo_device.tests.unit_tests.utils import fake_device_test_case @@ -34,10 +34,10 @@ class LevelControlClusterPwRpcTest(fake_device_test_case.FakeDeviceTestCase): def setUp(self): super().setUp() - self.fake_read = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.read) - self.fake_write = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.write) + self.fake_read = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.read) + self.fake_write = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.write) self.uut = level_control_pw_rpc.LevelControlClusterPwRpc( device_name=_FAKE_DEVICE_NAME, endpoint_id=_FAKE_ENDPOINT_ID, diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/occupancy_pw_rpc_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/occupancy_pw_rpc_test.py index 00b43d3..ca900f0 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/occupancy_pw_rpc_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/occupancy_pw_rpc_test.py @@ -15,7 +15,7 @@ """Matter cluster capability unit test for occupancy_pw_rpc module.""" from unittest import mock -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities.matter_clusters import occupancy_pw_rpc from gazoo_device.tests.unit_tests.utils import fake_device_test_case @@ -30,7 +30,7 @@ class OccupancyClusterPwRpcTest(fake_device_test_case.FakeDeviceTestCase): def setUp(self): super().setUp() self.fake_read = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.read) + spec=matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc.read) self.fake_read.return_value = mock.Mock(data_uint8=_FAKE_DATA) self.uut = occupancy_pw_rpc.OccupancyClusterPwRpc( device_name=_FAKE_DEVICE_NAME, diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/on_off_pw_rpc_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/on_off_pw_rpc_test.py index 27bf2f9..589b00d 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/on_off_pw_rpc_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/on_off_pw_rpc_test.py @@ -17,7 +17,7 @@ import gazoo_device from gazoo_device import errors -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities.matter_clusters import on_off_pw_rpc from gazoo_device.tests.unit_tests.utils import fake_device_test_case @@ -32,10 +32,10 @@ class OnOffClusterPwRpcTest(fake_device_test_case.FakeDeviceTestCase): def setUp(self): super().setUp() - self.fake_read = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.read) - self.fake_write = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.write) + self.fake_read = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.read) + self.fake_write = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.write) self.uut = on_off_pw_rpc.OnOffClusterPwRpc( device_name=_FAKE_DEVICE_NAME, endpoint_id=_FAKE_ENDPOINT_ID, diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/pressure_measurement_pw_rpc_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/pressure_measurement_pw_rpc_test.py index 9dbccae..00738d4 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/pressure_measurement_pw_rpc_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/pressure_measurement_pw_rpc_test.py @@ -14,14 +14,15 @@ """Matter cluster unit test for pressure_measurement_pw_rpc module.""" from unittest import mock - -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device import errors +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities.matter_clusters import pressure_measurement_pw_rpc from gazoo_device.tests.unit_tests.utils import fake_device_test_case _FAKE_DEVICE_NAME = "fake-device-name" _FAKE_ENDPOINT_ID = 1 _FAKE_DATA = 1 +_FAKE_DATA2 = 2 class PressureMeasurementClusterPwRpcTest( @@ -30,15 +31,17 @@ class PressureMeasurementClusterPwRpcTest( def setUp(self): super().setUp() - self.fake_read = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.read) + self.fake_read = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.read) self.fake_read.return_value = mock.Mock(data_int16=_FAKE_DATA) + self.fake_write = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.write) self.uut = ( pressure_measurement_pw_rpc.PressureMeasurementClusterPwRpc( device_name=_FAKE_DEVICE_NAME, endpoint_id=_FAKE_ENDPOINT_ID, read=self.fake_read, - write=None)) + write=self.fake_write)) def test_measured_value_attribute(self): """Verifies the measured_value attribute on success.""" @@ -55,6 +58,45 @@ def test_max_measured_value_bitmap_attribute(self): self.assertEqual(_FAKE_DATA, self.uut.max_measured_value) self.fake_read.assert_called_once() + def test_writing_measured_value_attribute_on_success(self): + """Verifies writing measured_value attribute on success.""" + self.uut.measured_value = _FAKE_DATA + self.fake_write.assert_called_once() + + @mock.patch.object( + pressure_measurement_pw_rpc.PressureMeasurementClusterPwRpc, + "_read_value", return_value=_FAKE_DATA2) + def test_writing_measured_value_attribute_on_failure(self, mock_read): + """Verifies writing measured_value attribute on failure.""" + with self.assertRaisesRegex(errors.DeviceError, "didn't change"): + self.uut.measured_value = _FAKE_DATA + + def test_writing_min_measured_value_attribute_on_success(self): + """Verifies writing min_measured_value attribute on success.""" + self.uut.min_measured_value = _FAKE_DATA + self.fake_write.assert_called_once() + + @mock.patch.object( + pressure_measurement_pw_rpc.PressureMeasurementClusterPwRpc, + "_read_value", return_value=_FAKE_DATA2) + def test_writing_min_measured_value_attribute_on_failure(self, mock_read): + """Verifies writing min_measured_value attribute on failure.""" + with self.assertRaisesRegex(errors.DeviceError, "didn't change"): + self.uut.min_measured_value = _FAKE_DATA + + def test_writing_max_measured_value_attribute_on_success(self): + """Verifies writing max_measured_value attribute on success.""" + self.uut.max_measured_value = _FAKE_DATA + self.fake_write.assert_called_once() + + @mock.patch.object( + pressure_measurement_pw_rpc.PressureMeasurementClusterPwRpc, + "_read_value", return_value=_FAKE_DATA2) + def test_writing_max_measured_value_attribute_on_failure(self, mock_read): + """Verifies writing max_measured_value attribute on failure.""" + with self.assertRaisesRegex(errors.DeviceError, "didn't change"): + self.uut.max_measured_value = _FAKE_DATA + if __name__ == "__main__": fake_device_test_case.main() diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/temperature_measurement_pw_rpc_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/temperature_measurement_pw_rpc_test.py index 4223db6..17c7e1a 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/temperature_measurement_pw_rpc_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_cluster_tests/temperature_measurement_pw_rpc_test.py @@ -14,14 +14,15 @@ """Matter cluster unit test for temperature_measurement_pw_rpc module.""" from unittest import mock - -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device import errors +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities.matter_clusters import temperature_measurement_pw_rpc from gazoo_device.tests.unit_tests.utils import fake_device_test_case _FAKE_DEVICE_NAME = "fake-device-name" _FAKE_ENDPOINT_ID = 1 _FAKE_DATA = 1 +_FAKE_DATA2 = 2 class TemperatureMeasurementClusterPwRpcTest( @@ -30,36 +31,67 @@ class TemperatureMeasurementClusterPwRpcTest( def setUp(self): super().setUp() - fake_read = mock.Mock( - spec=matter_endpoints_accessor.MatterEndpointsAccessor.read) + self.fake_read = mock.Mock( + spec=matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc.read) + self.fake_read.return_value = mock.Mock(data_int16=_FAKE_DATA) + self.fake_write = mock.Mock(spec=matter_endpoints_accessor_pw_rpc + .MatterEndpointsAccessorPwRpc.write) self.uut = ( temperature_measurement_pw_rpc.TemperatureMeasurementClusterPwRpc( device_name=_FAKE_DEVICE_NAME, endpoint_id=_FAKE_ENDPOINT_ID, - read=fake_read, - write=None)) + read=self.fake_read, + write=self.fake_write)) def test_measured_value_attribute(self): """Verifies the measured_value attribute on success.""" - self.uut._read.return_value = mock.Mock(data_int16=_FAKE_DATA) - self.assertEqual(_FAKE_DATA, self.uut.measured_value) + self.fake_read.assert_called_once() + + def test_writing_measured_value_attribute(self): + """Verifies writing measured_value attribute on success.""" + self.uut.measured_value = _FAKE_DATA + self.fake_write.assert_called_once() def test_min_measured_value_attribute(self): """Verifies the min_measured_value attribute on success.""" + self.assertEqual(_FAKE_DATA, self.uut.min_measured_value) + self.fake_read.assert_called_once() + + def test_writing_min_measured_value_attribute(self): + """Verifies writing min_measured_value attribute on success.""" + self.uut.min_measured_value = _FAKE_DATA + self.fake_write.assert_called_once() + + def test_max_measured_value_attribute(self): + """Verifies the max_measured_value attribute on success.""" + self.assertEqual(_FAKE_DATA, self.uut.max_measured_value) + self.fake_read.assert_called_once() + + def test_writing_max_measured_value_attribute(self): + """Verifies writing max_measured_value attribute on success.""" + self.uut.max_measured_value = _FAKE_DATA + self.fake_write.assert_called_once() + + def test_convert_attribute_constraint_value_convert(self): + """Verifies convert_attribute_constraint_value converting on success.""" fake_min_measured_value = ( temperature_measurement_pw_rpc._MIN_MEASURED_UPPERBOUND+_FAKE_DATA) expected_value = ( fake_min_measured_value - temperature_measurement_pw_rpc._ATTR_VAL_MAX) - self.uut._read.return_value = mock.Mock(data_int16=fake_min_measured_value) - - self.assertEqual(expected_value, self.uut.min_measured_value) + self.assertEqual( + expected_value, + temperature_measurement_pw_rpc._convert_attribute_constraint_value( + fake_min_measured_value, + temperature_measurement_pw_rpc._MIN_MEASURED_UPPERBOUND)) - def test_max_measured_value_bitmap_attribute(self): - """Verifies the max_measured_value attribute on success.""" - self.uut._read.return_value = mock.Mock(data_int16=_FAKE_DATA) - - self.assertEqual(_FAKE_DATA, self.uut.max_measured_value) + @mock.patch.object( + temperature_measurement_pw_rpc.TemperatureMeasurementClusterPwRpc, + "_read_value", return_value=_FAKE_DATA2) + def test_write_value_on_failure(self, mock_read): + """Verifies _write_value on failure when verification.""" + with self.assertRaisesRegex(errors.DeviceError, "didn't change"): + self.uut.measured_value = _FAKE_DATA if __name__ == "__main__": diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_controller_chip_tool_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_controller_chip_tool_test.py index 88f932a..d99e168 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_controller_chip_tool_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_controller_chip_tool_test.py @@ -30,6 +30,7 @@ def setUp(self): super().setUp() self.setup_fake_device_requirements("rpi_matter_controller-1234") self.device_config["persistent"]["console_port_name"] = "123.45.67.89" + self.device_config["options"]["matter_node_id"] = 1234 self.fake_responder.behavior_dict = ( raspberry_pi_matter_controller_device_logs.DEFAULT_BEHAVIOR.copy()) @@ -54,6 +55,8 @@ def test_commission_over_ble_wifi(self): password="wifi-password", setup_code=self._setup_code, long_discriminator=self._long_discriminator) + self.uut.get_manager().set_prop.assert_called_once_with( + self.uut.name, "matter_node_id", self._node_id) def test_commission_over_ble_wifi_with_hex(self): self.uut.matter_controller.commission( @@ -62,16 +65,22 @@ def test_commission_over_ble_wifi_with_hex(self): password="hex:776966692d70617373776f7264", setup_code=self._setup_code, long_discriminator=self._long_discriminator) + self.uut.get_manager().set_prop.assert_called_once_with( + self.uut.name, "matter_node_id", self._node_id) def test_commission_on_network(self): self.uut.matter_controller.commission( node_id=self._node_id, setup_code=self._setup_code) + self.uut.get_manager().set_prop.assert_called_once_with( + self.uut.name, "matter_node_id", self._node_id) def test_commission_on_network_long(self): self.uut.matter_controller.commission( node_id=self._node_id, setup_code=self._setup_code, long_discriminator=self._long_discriminator) + self.uut.get_manager().set_prop.assert_called_once_with( + self.uut.name, "matter_node_id", self._node_id) def test_commission_over_ble_thread(self): self.uut.matter_controller.commission( @@ -79,6 +88,8 @@ def test_commission_over_ble_thread(self): setup_code=self._setup_code, long_discriminator=self._long_discriminator, operational_dataset="abcd") + self.uut.get_manager().set_prop.assert_called_once_with( + self.uut.name, "matter_node_id", self._node_id) def test_commission_timeout_failure(self): """Commission method should raise DeviceError when setup code is invalid.""" @@ -96,41 +107,43 @@ def test_commission_over_ble_wifi_without_password(self): long_discriminator=self._long_discriminator) def test_decommission(self): - self.uut.matter_controller.decommission(node_id=self._node_id) + self.uut.matter_controller.decommission() + self.uut.get_manager().set_prop.assert_called_once_with( + self.uut.name, "matter_node_id", None) def test_decommission_timeout_failure(self): """Decommission method should raise DeviceError when node id is invalid.""" - invalid_node_id = "0000" + self.device_config["options"]["matter_node_id"] = 0000 with self.assertRaises(errors.DeviceError): - self.uut.matter_controller.decommission(node_id=invalid_node_id) + self.uut.matter_controller.decommission() def test_read_attribute_integer(self): self.assertEqual( - self.uut.matter_controller.read(self._node_id, self._endpoint_id, - self._cluster, "on-time"), 0) + self.uut.matter_controller.read(self._endpoint_id, self._cluster, + "on-time"), 0) def test_read_attribute_boolean(self): self.assertTrue( - self.uut.matter_controller.read(self._node_id, self._endpoint_id, - self._cluster, "on-off")) + self.uut.matter_controller.read(self._endpoint_id, self._cluster, + "on-off")) def test_write_attribute(self): - self.uut.matter_controller.write(self._node_id, self._endpoint_id, - self._cluster, "on-time", 100) + self.uut.matter_controller.write(self._endpoint_id, self._cluster, + "on-time", 100) def test_send_command(self): - self.uut.matter_controller.send(self._node_id, self._endpoint_id, - self._cluster, "toggle", []) + self.uut.matter_controller.send(self._endpoint_id, self._cluster, "toggle", + []) def test_write_attribute_non_zero_status_code(self): with self.assertRaises(errors.DeviceError): - self.uut.matter_controller.write(self._node_id, self._endpoint_id, - self._cluster, "non-existent-attr", 0) + self.uut.matter_controller.write(self._endpoint_id, self._cluster, + "non-existent-attr", 0) def test_send_command_non_zero_status_code(self): with self.assertRaises(errors.DeviceError): - self.uut.matter_controller.write(self._node_id, self._endpoint_id, - self._cluster, "non-existent-cmd", []) + self.uut.matter_controller.write(self._endpoint_id, self._cluster, + "non-existent-cmd", []) def test_upgrade(self): with mock.patch.object(file_transfer_scp.FileTransferScp, diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_endpoint_tests/matter_endpoints_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_endpoint_tests/matter_endpoints_test.py index e09d3fa..f1c81be 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_endpoint_tests/matter_endpoints_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_endpoint_tests/matter_endpoints_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Matter endpoints unit test.""" import itertools from unittest import mock @@ -28,14 +27,17 @@ # Trim the cluster capability names into alias names. _CLUSTER_NAMES = ( - cluster.get_capability_name().replace("_control_cluster", ""). - replace("_cluster", "") for cluster in - matter_endpoints_and_clusters.SUPPORTED_CLUSTERS) + cluster.get_capability_name().replace("_control_cluster", + "").replace("_cluster", "") + for cluster in matter_endpoints_and_clusters.SUPPORTED_CLUSTERS_PW_RPC) + +_ENDPOINT_AND_CLUSTER_PRODUCT = itertools.product( + matter_endpoints_and_clusters.SUPPORTED_ENDPOINTS_PW_RPC, _CLUSTER_NAMES) ENDPOINT_AND_CLUSTER_PAIR = [ dict(endpoint_class=endpoint_class, cluster_name=cluster_name) - for endpoint_class, cluster_name in itertools.product( - matter_endpoints_and_clusters.SUPPORTED_ENDPOINTS, _CLUSTER_NAMES)] + for endpoint_class, cluster_name in _ENDPOINT_AND_CLUSTER_PRODUCT +] class MatterEndpointsTest(fake_device_test_case.FakeDeviceTestCase): @@ -46,8 +48,8 @@ class MatterEndpointsTest(fake_device_test_case.FakeDeviceTestCase): @parameterized.parameters(ENDPOINT_AND_CLUSTER_PAIR) @mock.patch.object(endpoint_base.EndpointBase, "cluster_lazy_init") - def test_endpoint_and_cluster( - self, mock_cluster_lazy_init, endpoint_class, cluster_name): + def test_endpoint_and_cluster(self, mock_cluster_lazy_init, endpoint_class, + cluster_name): """Verifies the endpoint and cluster instance initialization.""" uut = endpoint_class( device_name=_FAKE_DEVICE_NAME, diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_endpoints_accessor_chip_tool_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_endpoints_accessor_chip_tool_test.py new file mode 100644 index 0000000..632e8db --- /dev/null +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_endpoints_accessor_chip_tool_test.py @@ -0,0 +1,129 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Unit tests for the MatterControllerChipTool capability.""" + +from gazoo_device import errors +from gazoo_device.auxiliary_devices import raspberry_pi_matter_controller +from gazoo_device.capabilities.matter_clusters import on_off_chip_tool +from gazoo_device.capabilities.matter_endpoints import on_off_light +from gazoo_device.capabilities.matter_endpoints.interfaces import endpoint_base +from gazoo_device.tests.unit_tests.utils import fake_device_test_case +from gazoo_device.tests.unit_tests.utils import raspberry_pi_matter_controller_device_logs + + +class MatterEndpointsAccessorChipToolCapabilityTests( + fake_device_test_case.FakeDeviceTestCase): + """Unit test for MatterEndpointsAccessorChipTool capability implementation.""" + + def setUp(self): + super().setUp() + self.setup_fake_device_requirements("rpi_matter_controller-1234") + self.device_config["persistent"]["console_port_name"] = "123.45.67.89" + self.fake_responder.behavior_dict = ( + raspberry_pi_matter_controller_device_logs.DEFAULT_BEHAVIOR.copy()) + + self.uut = raspberry_pi_matter_controller.RaspberryPiMatterController( + self.mock_manager, + self.device_config, + log_directory=self.artifacts_directory) + self._node_id = 1234 + self._endpoint = 1 + self.device_config["options"]["matter_node_id"] = self._node_id + + def test_get_endpoint(self): + """Tests get_endpoint.""" + self.assertIsInstance( + self.uut.matter_endpoints.get(self._endpoint), + on_off_light.OnOffLightEndpoint) + + def test_list_endpoints(self): + """Tests list.""" + endpoints = self.uut.matter_endpoints.list() + self.assertLen(endpoints, 1) + self.assertEqual(endpoints[self._endpoint], on_off_light.OnOffLightEndpoint) + + def test_get_endpoint_instance_by_class(self): + """Tests get_endpoint_instance_by_class.""" + self.assertEqual( + self.uut.matter_endpoints.get_endpoint_instance_by_class( + on_off_light.OnOffLightEndpoint), + self.uut.matter_endpoints.get(self._endpoint)) + + def test_get_invalid_endpoint(self): + """Tests get with invalid endpoint id.""" + with self.assertRaises(errors.DeviceError): + self.uut.matter_endpoints.get(42) + + def test_get_invalid_endpoint_instance_by_class(self): + """Tests get_endpoint_instance_by_class with invalid endpoint class.""" + with self.assertRaises(errors.DeviceError): + self.uut.matter_endpoints.get_endpoint_instance_by_class( + endpoint_base.EndpointBase) + + def test_reset(self): + """Tests reset.""" + self.uut.matter_endpoints.list() + self.assertNotEmpty(self.uut.matter_endpoints._endpoint_id_to_class) + + self.uut.matter_endpoints.get(self._endpoint) + self.assertNotEmpty(self.uut.matter_endpoints._endpoints) + + self.uut.matter_endpoints.reset() + self.assertEmpty(self.uut.matter_endpoints._endpoints) + self.assertEmpty(self.uut.matter_endpoints._endpoint_id_to_class) + self.assertEmpty(self.uut.matter_endpoints._endpoint_class_to_id) + self.assertEmpty(self.uut.matter_endpoints._endpoint_id_to_clusters) + self.assertEmpty(self.uut.matter_endpoints._endpoint_id_to_device_type_id) + + def test_has_endpoints(self): + """Tests has_endpoints.""" + self.assertTrue(self.uut.matter_endpoints.has_endpoints(["on_off_light"])) + + def test_has_endpoints_invalid(self): + """Tests has_endpoints with invalid endpoint name.""" + with self.assertRaises(errors.DeviceError): + self.uut.matter_endpoints.has_endpoints(["fake_endpoint"]) + + def test_get_supported_endpoints(self): + """Tests get_supported_endpoints.""" + supported_endpoints = self.uut.matter_endpoints.get_supported_endpoints() + self.assertNotEmpty(supported_endpoints) + self.assertIn("on_off_light", supported_endpoints) + + def test_get_supported_endpoints_and_clusters(self): + """Tests get_supported_endpoints_and_clusters.""" + self.assertDictEqual( + self.uut.matter_endpoints.get_supported_endpoints_and_clusters(), + {1: ["on_off_cluster"]}) + + def test_get_supported_endpoint_instances_and_cluster_flavors(self): + """Tests get_supported_endpoint_instances_and_cluster_flavors.""" + endpoints = self.uut.matter_endpoints.get_supported_endpoint_instances_and_cluster_flavors( + ) + self.assertNotEmpty(endpoints) + + key = next(iter(endpoints)) + self.assertIsInstance(key, on_off_light.OnOffLightEndpoint) + self.assertSetEqual(endpoints[key], + set([on_off_chip_tool.OnOffClusterChipTool])) + + def test_endpoint_id_to_clusters(self): + """Tests endpoint_id_to_clusters property.""" + self.assertEmpty(self.uut.matter_endpoints._endpoint_id_to_clusters) + self.assertNotEmpty(self.uut.matter_endpoints.endpoint_id_to_clusters) + + def test_endpoint_id_to_device_type_id(self): + """Tests endpoint_id_to_device_type_id property.""" + self.assertEmpty(self.uut.matter_endpoints._endpoint_id_to_device_type_id) + self.assertNotEmpty(self.uut.matter_endpoints.endpoint_id_to_device_type_id) diff --git a/gazoo_device/tests/unit_tests/capability_tests/matter_endpoints_accessor_test.py b/gazoo_device/tests/unit_tests/capability_tests/matter_endpoints_accessor_test.py index 36aa60f..a4a427f 100644 --- a/gazoo_device/tests/unit_tests/capability_tests/matter_endpoints_accessor_test.py +++ b/gazoo_device/tests/unit_tests/capability_tests/matter_endpoints_accessor_test.py @@ -11,13 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Capability unit test for matter_endpoints_accessor module.""" from unittest import mock from absl.testing import parameterized from gazoo_device import errors -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities import matter_endpoints_and_clusters from gazoo_device.capabilities.matter_endpoints import on_off_light from gazoo_device.capabilities.matter_endpoints import unsupported_endpoint @@ -29,6 +28,7 @@ _FAKE_ENDPOINT_ID = 0 _FAKE_DEVICE_TYPE_ID = 10 +_FAKE_CLUSTER_ID = 0 _FAKE_RPC_TIMEOUT_S = 10 _FAKE_DEVICE_NAME = "fake-device-name" _FAKE_ENDPOINT_NAME = "fake_endpoint_name" @@ -36,228 +36,240 @@ _FAKE_ENDPOINT_IDS = [_FAKE_ENDPOINT_ID] _FAKE_ENDPOINT_CLS = mock.Mock(DEVICE_TYPE_ID=_FAKE_DEVICE_TYPE_ID) _FAKE_ENDPOINT_INST = mock.Mock(spec=endpoint_base.EndpointBase) +_FAKE_CLUSTER_CLS = mock.Mock(CLUSTER_ID=_FAKE_CLUSTER_ID) _FAKE_ENDPOINT_ID_TO_CLS = {_FAKE_ENDPOINT_ID: _FAKE_ENDPOINT_CLS} _FAKE_ENDPOINT_CLS_TO_ID = {_FAKE_ENDPOINT_CLS: _FAKE_ENDPOINT_ID} -_FAKE_CLUSTER_ID = 0 +_FAKE_ENDPOINT_ID_TO_CLUSTERS = {_FAKE_ENDPOINT_ID: [_FAKE_CLUSTER_CLS]} +_FAKE_ENDPOINT_ID_TO_DEVICE_TYPE_ID = {_FAKE_ENDPOINT_ID: _FAKE_DEVICE_TYPE_ID} _FAKE_NOT_IMPLEMENTED_CLUSTER_ID = 1 -_FAKE_CLUSTER_CLS = mock.Mock(CLUSTER_ID=_FAKE_CLUSTER_ID) _FAKE_ATTRIBUTE_ID = 0 _FAKE_ATTRIBUTE_TYPE = attributes_service_pb2.AttributeType.ZCL_BOOLEAN_ATTRIBUTE_TYPE -class DescriptorServiceHandlerTest(fake_device_test_case.FakeDeviceTestCase): - """Unit test for _DescriptorServiceHandler.""" +class MatterEndpointsAccessorPwPpcTest( + fake_device_test_case.FakeDeviceTestCase): + """Unit test for MatterEndpointsAccessorPwRpc.""" def setUp(self): super().setUp() - self.fake_switchboard_call = mock.Mock() - self.handler = matter_endpoints_accessor._DescriptorServiceHandler( + self.fake_switchboard_call = mock.Mock( + spec=switchboard.SwitchboardDefault.call) + self.uut = matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc( device_name=_FAKE_DEVICE_NAME, switchboard_call=self.fake_switchboard_call, rpc_timeout_s=_FAKE_RPC_TIMEOUT_S) + _FAKE_ENDPOINT_CLS.return_value = _FAKE_ENDPOINT_INST @mock.patch.object( - matter_endpoints_accessor._DescriptorServiceHandler, - "_get_supported_clusters", return_value={_FAKE_CLUSTER_CLS,}) + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "get_supported_clusters", + return_value={ + _FAKE_CLUSTER_CLS, + }) @mock.patch.object( - matter_endpoints_accessor._DescriptorServiceHandler, - "_get_endpoint_class", side_effect=[_FAKE_ENDPOINT_CLS]) + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "get_endpoint_class", + side_effect=[_FAKE_ENDPOINT_CLS]) @mock.patch.object( - matter_endpoints_accessor._DescriptorServiceHandler, - "_get_supported_endpoint_ids", return_value=_FAKE_ENDPOINT_IDS) + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "get_supported_endpoint_ids", + return_value=_FAKE_ENDPOINT_IDS) def test_fetch_endpoints_and_clusters_on_success( - self, - mock_get_suppored_endpoint_ids, - mock_get_endpoint_cls, + self, mock_get_suppored_endpoint_ids, mock_get_endpoint_cls, mock_get_supported_clusters): """Verifies descriptor RPC service is triggered successfully.""" - self.handler.reset() + self.uut.reset() - self.handler._fetch_endpoints_and_clusters() + self.uut._fetch_endpoints_and_clusters() self.assertEqual( _FAKE_DEVICE_TYPE_ID, - self.handler.endpoint_id_to_class[_FAKE_ENDPOINT_ID].DEVICE_TYPE_ID) - self.assertIn(_FAKE_ENDPOINT_ID, self.handler.endpoint_class_to_id.values()) + self.uut.endpoint_id_to_class[_FAKE_ENDPOINT_ID].DEVICE_TYPE_ID) + self.assertIn(_FAKE_ENDPOINT_ID, self.uut.endpoint_class_to_id.values()) self.assertEqual( _FAKE_CLUSTER_ID, - self.handler.endpoint_id_to_clusters[_FAKE_ENDPOINT_ID].pop(). - CLUSTER_ID) + self.uut.endpoint_id_to_clusters[_FAKE_ENDPOINT_ID].pop().CLUSTER_ID) mock_get_suppored_endpoint_ids.assert_called_once() mock_get_endpoint_cls.assert_called_once() mock_get_supported_clusters.assert_called_once() def test_get_supported_endpoint_ids_on_success(self): - """Verifies _get_supported_endpoint_ids method on success.""" + """Verifies get_supported_endpoint_ids method on success.""" fake_endpoint = descriptor_service_pb2.Endpoint(endpoint=_FAKE_ENDPOINT_ID) fake_supported_endpoints = [fake_endpoint.SerializeToString()] self.fake_switchboard_call.return_value = True, fake_supported_endpoints - self.assertEqual( - [_FAKE_ENDPOINT_ID], self.handler._get_supported_endpoint_ids()) + self.assertEqual([_FAKE_ENDPOINT_ID], self.uut.get_supported_endpoint_ids()) def test_get_supported_endpoint_ids_on_failure_false_ack(self): - """Verifies _get_supported_endpoint_ids on failure with false ack.""" + """Verifies get_supported_endpoint_ids on failure with false ack.""" self.fake_switchboard_call.return_value = False, [] - with self.assertRaisesRegex( - errors.DeviceError, "getting Descriptor PartsList failed"): - self.handler._get_supported_endpoint_ids() + with self.assertRaisesRegex(errors.DeviceError, + "getting Descriptor PartsList failed"): + self.uut.get_supported_endpoint_ids() @mock.patch.object( - matter_endpoints_and_clusters.MATTER_DEVICE_TYPE_ID_TO_CLASS, - "get", return_value=_FAKE_ENDPOINT_CLS) + matter_endpoints_and_clusters.MATTER_DEVICE_TYPE_ID_TO_CLASS_PW_RPC, + "get", + return_value=_FAKE_ENDPOINT_CLS) def test_get_endpoint_class_on_success(self, mock_mapping): - """Verifies _get_endpoint_class on success.""" + """Verifies get_endpoint_class on success.""" fake_device_type = descriptor_service_pb2.DeviceType( device_type=_FAKE_DEVICE_TYPE_ID) fake_device_types = [fake_device_type.SerializeToString()] self.fake_switchboard_call.return_value = True, fake_device_types self.assertEqual(_FAKE_ENDPOINT_CLS, - self.handler._get_endpoint_class(_FAKE_ENDPOINT_ID)) - self.assertEqual( - _FAKE_DEVICE_TYPE_ID, - self.handler.endpoint_id_to_device_type_id[_FAKE_ENDPOINT_ID]) + self.uut.get_endpoint_class(_FAKE_ENDPOINT_ID)) + self.assertEqual(_FAKE_DEVICE_TYPE_ID, + self.uut.endpoint_id_to_device_type_id[_FAKE_ENDPOINT_ID]) def test_get_endpoint_class_on_failure_false_ack(self): - """Verifies _get_endpoint_class on failure with false ack.""" + """Verifies get_endpoint_class on failure with false ack.""" self.fake_switchboard_call.return_value = False, [] - with self.assertRaisesRegex( - errors.DeviceError, "getting Descriptor DeviceTypeList failed"): - self.handler._get_endpoint_class(_FAKE_ENDPOINT_ID) + with self.assertRaisesRegex(errors.DeviceError, + "getting Descriptor DeviceTypeList failed"): + self.uut.get_endpoint_class(_FAKE_ENDPOINT_ID) def test_reset_method_on_success(self): """Verifies reset method on success.""" - self.handler.reset() + self.uut.reset() - self.assertEqual({}, self.handler._endpoint_id_to_class) - self.assertEqual({}, self.handler._endpoint_class_to_id) - self.assertEqual({}, self.handler._endpoint_id_to_clusters) + self.assertEqual({}, self.uut._endpoint_id_to_class) + self.assertEqual({}, self.uut._endpoint_class_to_id) + self.assertEqual({}, self.uut._endpoint_id_to_clusters) @parameterized.parameters( dict(cluster_id=_FAKE_CLUSTER_ID, cluster_class=_FAKE_CLUSTER_CLS), dict(cluster_id=_FAKE_NOT_IMPLEMENTED_CLUSTER_ID, cluster_class=None)) - @mock.patch.object(matter_endpoints_and_clusters.CLUSTER_ID_TO_CLASS, "get") - def test_get_supported_clusters_on_success( - self, mock_get, cluster_id, cluster_class): - """Verifies _get_supported_clusters method on success.""" + @mock.patch.object(matter_endpoints_and_clusters.CLUSTER_ID_TO_CLASS_PW_RPC, + "get") + def test_get_supported_clusters_on_success(self, mock_get, cluster_id, + cluster_class): + """Verifies get_supported_clusters method on success.""" mock_get.return_value = cluster_class fake_cluster = descriptor_service_pb2.Cluster(cluster_id=cluster_id) fake_clusters = [fake_cluster.SerializeToString()] self.fake_switchboard_call.return_value = True, fake_clusters expected_cluster_classes = ( - set() if cluster_class is None else {cluster_class,}) + set() if cluster_class is None else { + cluster_class, + }) self.assertEqual( expected_cluster_classes, - self.handler._get_supported_clusters(endpoint_id=_FAKE_ENDPOINT_ID)) + self.uut.get_supported_clusters(endpoint_id=_FAKE_ENDPOINT_ID)) def test_get_supported_clusters_on_failure(self): - """Verifies _get_supported_clusters method on failure.""" + """Verifies get_supported_clusters method on failure.""" self.fake_switchboard_call.return_value = False, [] - with self.assertRaisesRegex( - errors.DeviceError, "getting Descriptor ServerList failed"): - self.handler._get_supported_clusters(endpoint_id=_FAKE_ENDPOINT_ID) - - -class MatterEndpointsAccessorTest(fake_device_test_case.FakeDeviceTestCase): - """Unit test for MatterEndpointsAccessor.""" - - def setUp(self): - super().setUp() - handler_patcher = mock.patch.object(matter_endpoints_accessor, - "_DescriptorServiceHandler") - handler_class = handler_patcher.start() - self.addCleanup(handler_patcher.stop) - self.fake_handler = handler_class.return_value - self.fake_swtichboard_call = mock.Mock( - spec=switchboard.SwitchboardDefault.call) - self.uut = matter_endpoints_accessor.MatterEndpointsAccessor( - device_name=_FAKE_DEVICE_NAME, - switchboard_call=self.fake_swtichboard_call, - rpc_timeout_s=_FAKE_RPC_TIMEOUT_S) - _FAKE_ENDPOINT_CLS.return_value = _FAKE_ENDPOINT_INST + with self.assertRaisesRegex(errors.DeviceError, + "getting Descriptor ServerList failed"): + self.uut.get_supported_clusters(endpoint_id=_FAKE_ENDPOINT_ID) - def test_get_method_on_success_with_supported_endpoint(self): + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "_fetch_endpoints_and_clusters") + def test_get_method_on_success_with_supported_endpoint(self, mock_fetch): """Verifies the get endpoint method on success with supported endpoint.""" - self.fake_handler.endpoint_id_to_class = _FAKE_ENDPOINT_ID_TO_CLS + self.uut._endpoint_id_to_class = _FAKE_ENDPOINT_ID_TO_CLS + self.uut._endpoint_id_to_clusters = _FAKE_ENDPOINT_ID_TO_CLUSTERS + self.uut._endpoint_id_to_device_type_id = _FAKE_ENDPOINT_ID_TO_DEVICE_TYPE_ID self.uut._endpoints.clear() + _FAKE_ENDPOINT_CLS.return_value = _FAKE_ENDPOINT_INST self.assertEqual(_FAKE_ENDPOINT_INST, self.uut.get(_FAKE_ENDPOINT_ID)) - def test_get_method_on_success_with_unsupported_endpoint(self): + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "_fetch_endpoints_and_clusters") + def test_get_method_on_success_with_unsupported_endpoint(self, mock_fetch): """Verifies the get endpoint method on success with unsupported endpoint.""" - self.fake_handler.endpoint_id_to_class = { - _FAKE_ENDPOINT_ID: unsupported_endpoint.UnsupportedEndpoint} + self.uut._endpoint_id_to_class = { + _FAKE_ENDPOINT_ID: unsupported_endpoint.UnsupportedEndpoint + } + self.uut._endpoint_id_to_clusters = _FAKE_ENDPOINT_ID_TO_CLUSTERS + self.uut._endpoint_id_to_device_type_id = _FAKE_ENDPOINT_ID_TO_DEVICE_TYPE_ID self.uut._endpoints.clear() - self.assertIsInstance(self.uut.get(_FAKE_ENDPOINT_ID), - unsupported_endpoint.UnsupportedEndpoint) + self.assertIsInstance( + self.uut.get(_FAKE_ENDPOINT_ID), + unsupported_endpoint.UnsupportedEndpoint) - def test_get_method_on_failure_invalid_id(self): + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "_fetch_endpoints_and_clusters") + def test_get_method_on_failure_invalid_id(self, mock_fetch): """Verifies get endpoint method on failure for invalid endpoint ID.""" - self.fake_handler.endpoint_id_to_class = {} - self.uut._endpoints.clear() + self.uut._endpoint_id_to_class = {} with self.assertRaisesRegex( errors.DeviceError, f"Endpoint ID {_FAKE_ENDPOINT_ID} on " f"{_FAKE_DEVICE_NAME} does not exist"): self.uut.get(_FAKE_ENDPOINT_ID) - def test_list_endpoints(self): + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "_fetch_endpoints_and_clusters") + def test_list_endpoints(self, mock_fetch): """Verifies the list endpoints method on success.""" - self.fake_handler.endpoint_id_to_class = _FAKE_ENDPOINT_ID_TO_CLS - + self.uut._endpoint_id_to_class = _FAKE_ENDPOINT_ID_TO_CLS self.assertEqual(_FAKE_ENDPOINT_ID_TO_CLS, self.uut.list()) @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, - "get", return_value=_FAKE_ENDPOINT_INST) - def test_get_endpoint_instance_by_class_on_success(self, mock_get): + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "get", + return_value=_FAKE_ENDPOINT_INST) + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "_fetch_endpoints_and_clusters") + def test_get_endpoint_instance_by_class_on_success(self, mock_fetch, + mock_get): """Verifies get_endpoint_instance_by_class method on success.""" - self.fake_handler.endpoint_class_to_id = _FAKE_ENDPOINT_CLS_TO_ID + self.uut._endpoint_class_to_id = _FAKE_ENDPOINT_CLS_TO_ID self.assertEqual( _FAKE_ENDPOINT_INST, self.uut.get_endpoint_instance_by_class(_FAKE_ENDPOINT_CLS)) mock_get.assert_called_once() - def test_get_endpoint_instance_by_class_on_failure(self): + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "_fetch_endpoints_and_clusters") + def test_get_endpoint_instance_by_class_on_failure(self, mock_fetch): """Verifies get_endpoint_instance_by_class with unsupported endpoint.""" - self.fake_handler.endpoint_class_to_id = {} + self.uut._endpoint_class_to_id = {} with self.assertRaisesRegex(errors.DeviceError, "is not supported on"): self.uut.get_endpoint_instance_by_class(_FAKE_ENDPOINT_CLS) - def test_reset_method_on_success(self): - """Verifies reset method on success.""" - self.uut.reset() - - self.fake_handler.reset.assert_called_once() - @parameterized.parameters( dict(supported_endpoint=on_off_light.OnOffLightEndpoint, expected=True), dict(supported_endpoint=None, expected=False)) - @mock.patch.object(matter_endpoints_accessor.MatterEndpointsAccessor, "list") - def test_has_endpoints_on_success( - self, mock_list, supported_endpoint, expected): + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "list") + def test_has_endpoints_on_success(self, mock_list, supported_endpoint, + expected): """Verifies has_endpoints method on success.""" mock_list.return_value = {_FAKE_ENDPOINT_ID: supported_endpoint} endpoint_name = on_off_light.OnOffLightEndpoint.get_capability_name() self.assertEqual(expected, self.uut.has_endpoints([endpoint_name])) - def test_has_endpoints_on_failure_invalid_endpoint(self): + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, + "_fetch_endpoints_and_clusters") + def test_has_endpoints_on_failure_invalid_endpoint(self, mock_fetch): """Verifies has_endpoints method on failure with invalid endpoint.""" error_msg = f"Endpoint {_FAKE_ENDPOINT_NAME} is not recognized" with self.assertRaisesRegex(errors.DeviceError, error_msg): self.uut.has_endpoints([_FAKE_ENDPOINT_NAME]) @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get_supported_endpoint_flavors") def test_get_supported_endpoints_on_success( self, mock_get_supported_endpoint_flavors): @@ -268,7 +280,8 @@ def test_get_supported_endpoints_on_success( self.assertEqual([_FAKE_ENDPOINT_NAME], self.uut.get_supported_endpoints()) - @mock.patch.object(matter_endpoints_accessor.MatterEndpointsAccessor, "list") + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "list") def test_get_supported_endpoint_flavors_on_success(self, mock_list): """Verifies get_supported_endpoint_flavors method on success.""" fake_endpoint = mock.Mock(spec=endpoint_base.EndpointBase) @@ -276,27 +289,32 @@ def test_get_supported_endpoint_flavors_on_success(self, mock_list): self.assertEqual([fake_endpoint], self.uut.get_supported_endpoint_flavors()) - @mock.patch.object(matter_endpoints_accessor.MatterEndpointsAccessor, "get") - @mock.patch.object(matter_endpoints_accessor.MatterEndpointsAccessor, "list") + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get") + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "list") def test_get_supported_endpoints_and_clusters(self, mock_list, mock_get): """Verifies get_supported_endpoints_and_clusters method on success.""" mock_list.return_value = [_FAKE_ENDPOINT_ID] mock_get.return_value.get_supported_clusters.return_value = [ - _FAKE_CLUSTER_NAME] + _FAKE_CLUSTER_NAME + ] - self.assertEqual( - {_FAKE_ENDPOINT_ID: [_FAKE_CLUSTER_NAME]}, - self.uut.get_supported_endpoints_and_clusters()) + self.assertEqual({_FAKE_ENDPOINT_ID: [_FAKE_CLUSTER_NAME]}, + self.uut.get_supported_endpoints_and_clusters()) - @mock.patch.object(matter_endpoints_accessor.MatterEndpointsAccessor, "get") - @mock.patch.object(matter_endpoints_accessor.MatterEndpointsAccessor, "list") + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get") + @mock.patch.object( + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "list") def test_get_supported_endpoint_instances_and_cluster_flavors( self, mock_list, mock_get): """Verifies get_supported_endpoint_instances_and_cluster_flavors method.""" mock_list.return_value = [_FAKE_ENDPOINT_ID] mock_get.return_value = _FAKE_ENDPOINT_INST _FAKE_ENDPOINT_INST.get_supported_cluster_flavors.return_value = [ - _FAKE_CLUSTER_CLS] + _FAKE_CLUSTER_CLS + ] expected_mapping = {_FAKE_ENDPOINT_INST: [_FAKE_CLUSTER_CLS]} self.assertEqual( @@ -306,49 +324,53 @@ def test_get_supported_endpoint_instances_and_cluster_flavors( def test_ember_api_read_on_success(self): """Verifies Ember API read method on success.""" data = attributes_service_pb2.AttributeData(data_bool=True) - self.fake_swtichboard_call.return_value = True, data.SerializeToString() + self.fake_switchboard_call.return_value = True, data.SerializeToString() - self.assertEqual(data, - self.uut.read( - endpoint_id=_FAKE_ENDPOINT_ID, - cluster_id=_FAKE_CLUSTER_ID, - attribute_id=_FAKE_ATTRIBUTE_ID, - attribute_type=_FAKE_ATTRIBUTE_TYPE)) + self.assertEqual( + data, + self.uut.read( + endpoint_id=_FAKE_ENDPOINT_ID, + cluster_id=_FAKE_CLUSTER_ID, + attribute_id=_FAKE_ATTRIBUTE_ID, + attribute_type=_FAKE_ATTRIBUTE_TYPE)) def test_ember_api_read_on_failure(self): """Verifies Ember API read method on failure.""" - self.fake_swtichboard_call.return_value = False, None + self.fake_switchboard_call.return_value = False, None error_message = f"Device {_FAKE_DEVICE_NAME} reading attribute" with self.assertRaisesRegex(errors.DeviceError, error_message): - self.uut.read(endpoint_id=_FAKE_ENDPOINT_ID, - cluster_id=_FAKE_CLUSTER_ID, - attribute_id=_FAKE_ATTRIBUTE_ID, - attribute_type=_FAKE_ATTRIBUTE_TYPE) + self.uut.read( + endpoint_id=_FAKE_ENDPOINT_ID, + cluster_id=_FAKE_CLUSTER_ID, + attribute_id=_FAKE_ATTRIBUTE_ID, + attribute_type=_FAKE_ATTRIBUTE_TYPE) def test_ember_api_write_on_success(self): """Verifies Ember API write method on success.""" - self.fake_swtichboard_call.return_value = True, None + self.fake_switchboard_call.return_value = True, None - self.uut.write(endpoint_id=_FAKE_ENDPOINT_ID, - cluster_id=_FAKE_CLUSTER_ID, - attribute_id=_FAKE_ATTRIBUTE_ID, - attribute_type=_FAKE_ATTRIBUTE_TYPE, - data_bool=True) + self.uut.write( + endpoint_id=_FAKE_ENDPOINT_ID, + cluster_id=_FAKE_CLUSTER_ID, + attribute_id=_FAKE_ATTRIBUTE_ID, + attribute_type=_FAKE_ATTRIBUTE_TYPE, + data_bool=True) - self.fake_swtichboard_call.assert_called_once() + self.fake_switchboard_call.assert_called_once() def test_ember_api_write_on_failure(self): """Verifies Ember API write method on failure.""" - self.fake_swtichboard_call.return_value = False, None + self.fake_switchboard_call.return_value = False, None error_message = f"Device {_FAKE_DEVICE_NAME} writing data" with self.assertRaisesRegex(errors.DeviceError, error_message): - self.uut.write(endpoint_id=_FAKE_ENDPOINT_ID, - cluster_id=_FAKE_CLUSTER_ID, - attribute_id=_FAKE_ATTRIBUTE_ID, - attribute_type=_FAKE_ATTRIBUTE_TYPE, - data_bool=True) + self.uut.write( + endpoint_id=_FAKE_ENDPOINT_ID, + cluster_id=_FAKE_CLUSTER_ID, + attribute_id=_FAKE_ATTRIBUTE_ID, + attribute_type=_FAKE_ATTRIBUTE_TYPE, + data_bool=True) if __name__ == "__main__": diff --git a/gazoo_device/tests/unit_tests/matter_device_base_test.py b/gazoo_device/tests/unit_tests/matter_device_base_test.py index 48a5282..9307dbb 100644 --- a/gazoo_device/tests/unit_tests/matter_device_base_test.py +++ b/gazoo_device/tests/unit_tests/matter_device_base_test.py @@ -21,7 +21,7 @@ from gazoo_device import errors from gazoo_device.base_classes import matter_device_base from gazoo_device.capabilities import device_power_default -from gazoo_device.capabilities import matter_endpoints_accessor +from gazoo_device.capabilities import matter_endpoints_accessor_pw_rpc from gazoo_device.capabilities import pwrpc_common_default from gazoo_device.capabilities.matter_endpoints import color_temperature_light from gazoo_device.capabilities.matter_endpoints import dimmable_light @@ -107,7 +107,7 @@ def test_is_connected_true(self, mock_exists): mock_exists.assert_called_once() @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get_endpoint_instance_by_class", return_value=_FAKE_ENDPOINT_INST) @mock.patch.object(usb_utils, "get_device_info") @@ -235,7 +235,7 @@ def test_pw_rpc_button_capability(self): # *************** Test cases for Matter endpoint aliases *************** # @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get_endpoint_instance_by_class") def test_color_temperature_light_alias(self, mock_get_endpoint): """Verifies color_temperature_light endpoint alias on success.""" @@ -244,7 +244,7 @@ def test_color_temperature_light_alias(self, mock_get_endpoint): color_temperature_light.ColorTemperatureLightEndpoint) @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get_endpoint_instance_by_class") def test_dimmable_light_alias(self, mock_get_endpoint): """Verifies dimmable_light endpoint alias on success.""" @@ -253,7 +253,7 @@ def test_dimmable_light_alias(self, mock_get_endpoint): dimmable_light.DimmableLightEndpoint) @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get_endpoint_instance_by_class") def test_door_lock_alias(self, mock_get_endpoint): """Verifies door_lock endpoint alias on success.""" @@ -261,7 +261,7 @@ def test_door_lock_alias(self, mock_get_endpoint): mock_get_endpoint.assert_called_once_with(door_lock.DoorLockEndpoint) @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get_endpoint_instance_by_class") def test_on_off_light_alias(self, mock_get_endpoint): """Verifies on_off_light endpoint alias on success.""" @@ -269,7 +269,7 @@ def test_on_off_light_alias(self, mock_get_endpoint): mock_get_endpoint.assert_called_once_with(on_off_light.OnOffLightEndpoint) @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get_endpoint_instance_by_class") def test_pressure_sensor_alias(self, mock_get_endpoint): """Verifies pressure_sensor endpoint alias on success.""" @@ -278,7 +278,7 @@ def test_pressure_sensor_alias(self, mock_get_endpoint): pressure_sensor.PressureSensorEndpoint) @mock.patch.object( - matter_endpoints_accessor.MatterEndpointsAccessor, + matter_endpoints_accessor_pw_rpc.MatterEndpointsAccessorPwRpc, "get_endpoint_instance_by_class") def test_temperature_sensor_alias(self, mock_get_endpoint): """Verifies temperature_sensor endpoint alias on success.""" diff --git a/gazoo_device/tests/unit_tests/raspberry_pi_matter_controller_device_test.py b/gazoo_device/tests/unit_tests/raspberry_pi_matter_controller_device_test.py index ec0be0d..2706382 100644 --- a/gazoo_device/tests/unit_tests/raspberry_pi_matter_controller_device_test.py +++ b/gazoo_device/tests/unit_tests/raspberry_pi_matter_controller_device_test.py @@ -13,8 +13,10 @@ # limitations under the License. """Unit tests for the raspberry_pi_matter_controller module.""" +from gazoo_device import errors from gazoo_device.auxiliary_devices import raspberry_pi_matter_controller from gazoo_device.base_classes import raspbian_device +from gazoo_device.capabilities.matter_endpoints import on_off_light from gazoo_device.tests.unit_tests.utils import fake_device_test_case from gazoo_device.tests.unit_tests.utils import raspberry_pi_matter_controller_device_logs @@ -27,6 +29,7 @@ def setUp(self): super().setUp() self.setup_fake_device_requirements("rpi_matter_controller-1234") self.device_config["persistent"]["console_port_name"] = "123.45.67.89" + self.device_config["options"]["matter_node_id"] = 1234 self.fake_responder.behavior_dict = ( raspberry_pi_matter_controller_device_logs.DEFAULT_BEHAVIOR.copy()) @@ -46,6 +49,19 @@ def test_has_chip_tool_command(self): def test_initialize_matter_controller_capability(self): self.assertIsNotNone(self.uut.matter_controller) + def test_initialize_matter_endpoints_accessor_capability(self): + self.assertIsNotNone(self.uut.matter_endpoints) + + def test_initialize_matter_endpoints_accessor_aliases(self): + self.assertIsInstance(self.uut.on_off_light, + on_off_light.OnOffLightEndpoint) + + def test_initialize_matter_endpoints_with_no_matter_node_id(self): + """Tests matter_endpoints throws DeviceError with no commissioned device.""" + del self.device_config["options"]["matter_node_id"] + with self.assertRaises(errors.DeviceError): + _ = self.uut.matter_endpoints + if __name__ == "__main__": fake_device_test_case.main() diff --git a/gazoo_device/tests/unit_tests/utils/raspberry_pi_matter_controller_device_logs.py b/gazoo_device/tests/unit_tests/utils/raspberry_pi_matter_controller_device_logs.py index 4a9cd25..58ddcb5 100644 --- a/gazoo_device/tests/unit_tests/utils/raspberry_pi_matter_controller_device_logs.py +++ b/gazoo_device/tests/unit_tests/utils/raspberry_pi_matter_controller_device_logs.py @@ -322,6 +322,202 @@ """), "code": 1, +}, { + "cmd": + "/usr/local/bin/chip-tool descriptor read parts-list 1234 0", + "resp": + textwrap.dedent(""" + [1653012051.833688][1030538:1030543] CHIP:EM: Removed CHIP MessageCounter:9561517 from RetransTable on exchange 19272i + [1653012051.833755][1030538:1030543] CHIP:DMG: ReportDataMessage = + [1653012051.833797][1030538:1030543] CHIP:DMG: { + [1653012051.833819][1030538:1030543] CHIP:DMG: AttributeReportIBs = + [1653012051.833860][1030538:1030543] CHIP:DMG: [ + [1653012051.833886][1030538:1030543] CHIP:DMG: AttributeReportIB = + [1653012051.833931][1030538:1030543] CHIP:DMG: { + [1653012051.833959][1030538:1030543] CHIP:DMG: AttributeDataIB = + [1653012051.834003][1030538:1030543] CHIP:DMG: { + [1653012051.834045][1030538:1030543] CHIP:DMG: DataVersion = 0xc700158c, + [1653012051.834077][1030538:1030543] CHIP:DMG: AttributePathIB = + [1653012051.834120][1030538:1030543] CHIP:DMG: { + [1653012051.834162][1030538:1030543] CHIP:DMG: Endpoint = 0x0, + [1653012051.834206][1030538:1030543] CHIP:DMG: Cluster = 0x1d, + [1653012051.834242][1030538:1030543] CHIP:DMG: Attribute = 0x0000_0003, + [1653012051.834285][1030538:1030543] CHIP:DMG: } + [1653012051.834321][1030538:1030543] CHIP:DMG: + [1653012051.834362][1030538:1030543] CHIP:DMG: Data = [ + [1653012051.834396][1030538:1030543] CHIP:DMG: + [1653012051.834439][1030538:1030543] CHIP:DMG: ], + [1653012051.834476][1030538:1030543] CHIP:DMG: }, + [1653012051.834510][1030538:1030543] CHIP:DMG: + [1653012051.834546][1030538:1030543] CHIP:DMG: }, + [1653012051.834586][1030538:1030543] CHIP:DMG: + [1653012051.834619][1030538:1030543] CHIP:DMG: AttributeReportIB = + [1653012051.834654][1030538:1030543] CHIP:DMG: { + [1653012051.834690][1030538:1030543] CHIP:DMG: AttributeDataIB = + [1653012051.834723][1030538:1030543] CHIP:DMG: { + [1653012051.834763][1030538:1030543] CHIP:DMG: DataVersion = 0xc700158c, + [1653012051.834795][1030538:1030543] CHIP:DMG: AttributePathIB = + [1653012051.834838][1030538:1030543] CHIP:DMG: { + [1653012051.834872][1030538:1030543] CHIP:DMG: Endpoint = 0x0, + [1653012051.834919][1030538:1030543] CHIP:DMG: Cluster = 0x1d, + [1653012051.834958][1030538:1030543] CHIP:DMG: Attribute = 0x0000_0003, + [1653012051.835001][1030538:1030543] CHIP:DMG: ListIndex = Null, + [1653012051.835033][1030538:1030543] CHIP:DMG: } + [1653012051.835077][1030538:1030543] CHIP:DMG: + [1653012051.835110][1030538:1030543] CHIP:DMG: Data = 1, + [1653012051.835155][1030538:1030543] CHIP:DMG: }, + [1653012051.835189][1030538:1030543] CHIP:DMG: + [1653012051.835216][1030538:1030543] CHIP:DMG: }, + [1653012051.835265][1030538:1030543] CHIP:DMG: + [1653012051.835975][1030538:1030543] CHIP:DMG: ], + [1653012051.836024][1030538:1030543] CHIP:DMG: + [1653012051.836050][1030538:1030543] CHIP:DMG: SuppressResponse = true, + [1653012051.836085][1030538:1030543] CHIP:DMG: InteractionModelRevision = 1 + [1653012051.836108][1030538:1030543] CHIP:DMG: } + [1653012051.836478][1030538:1030543] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_001D Attribute 0x0000_0003 DataVersion: 3338671500 + [1653012051.836538][1030538:1030543] CHIP:TOO: parts list: 1 entries + [1653012051.836568][1030538:1030543] CHIP:TOO: [1]: 1 + """), + "code": + 0, +}, { + "cmd": + "/usr/local/bin/chip-tool descriptor read device-list 1234 1", + "resp": + textwrap.dedent(""" + [1653012107.332640][1030544:1030549] CHIP:EM: Removed CHIP MessageCounter:519807 from RetransTable on exchange 35121i + [1653012107.332703][1030544:1030549] CHIP:DMG: ReportDataMessage = + [1653012107.332744][1030544:1030549] CHIP:DMG: { + [1653012107.332777][1030544:1030549] CHIP:DMG: AttributeReportIBs = + [1653012107.332809][1030544:1030549] CHIP:DMG: [ + [1653012107.332833][1030544:1030549] CHIP:DMG: AttributeReportIB = + [1653012107.332877][1030544:1030549] CHIP:DMG: { + [1653012107.332903][1030544:1030549] CHIP:DMG: AttributeDataIB = + [1653012107.332947][1030544:1030549] CHIP:DMG: { + [1653012107.332988][1030544:1030549] CHIP:DMG: DataVersion = 0xd1eb3728, + [1653012107.333020][1030544:1030549] CHIP:DMG: AttributePathIB = + [1653012107.333062][1030544:1030549] CHIP:DMG: { + [1653012107.333110][1030544:1030549] CHIP:DMG: Endpoint = 0x1, + [1653012107.333145][1030544:1030549] CHIP:DMG: Cluster = 0x1d, + [1653012107.333193][1030544:1030549] CHIP:DMG: Attribute = 0x0000_0000, + [1653012107.333238][1030544:1030549] CHIP:DMG: } + [1653012107.333279][1030544:1030549] CHIP:DMG: + [1653012107.333311][1030544:1030549] CHIP:DMG: Data = [ + [1653012107.333350][1030544:1030549] CHIP:DMG: + [1653012107.333396][1030544:1030549] CHIP:DMG: ], + [1653012107.333432][1030544:1030549] CHIP:DMG: }, + [1653012107.333475][1030544:1030549] CHIP:DMG: + [1653012107.333502][1030544:1030549] CHIP:DMG: }, + [1653012107.333553][1030544:1030549] CHIP:DMG: + [1653012107.333586][1030544:1030549] CHIP:DMG: AttributeReportIB = + [1653012107.333622][1030544:1030549] CHIP:DMG: { + [1653012107.333657][1030544:1030549] CHIP:DMG: AttributeDataIB = + [1653012107.333688][1030544:1030549] CHIP:DMG: { + [1653012107.333730][1030544:1030549] CHIP:DMG: DataVersion = 0xd1eb3728, + [1653012107.333770][1030544:1030549] CHIP:DMG: AttributePathIB = + [1653012107.333804][1030544:1030549] CHIP:DMG: { + [1653012107.333846][1030544:1030549] CHIP:DMG: Endpoint = 0x1, + [1653012107.333892][1030544:1030549] CHIP:DMG: Cluster = 0x1d, + [1653012107.333931][1030544:1030549] CHIP:DMG: Attribute = 0x0000_0000, + [1653012107.333973][1030544:1030549] CHIP:DMG: ListIndex = Null, + [1653012107.334017][1030544:1030549] CHIP:DMG: } + [1653012107.334053][1030544:1030549] CHIP:DMG: + [1653012107.334093][1030544:1030549] CHIP:DMG: Data = + [1653012107.334137][1030544:1030549] CHIP:DMG: { + [1653012107.334170][1030544:1030549] CHIP:DMG: 0x0 = 256, + [1653012107.334219][1030544:1030549] CHIP:DMG: 0x1 = 1, + [1653012107.334265][1030544:1030549] CHIP:DMG: }, + [1653012107.334304][1030544:1030549] CHIP:DMG: }, + [1653012107.334341][1030544:1030549] CHIP:DMG: + [1653012107.334375][1030544:1030549] CHIP:DMG: }, + [1653012107.334404][1030544:1030549] CHIP:DMG: + [1653012107.334424][1030544:1030549] CHIP:DMG: ], + [1653012107.334465][1030544:1030549] CHIP:DMG: + [1653012107.334491][1030544:1030549] CHIP:DMG: SuppressResponse = true, + [1653012107.334524][1030544:1030549] CHIP:DMG: InteractionModelRevision = 1 + [1653012107.334558][1030544:1030549] CHIP:DMG: } + [1653012107.334866][1030544:1030549] CHIP:TOO: Endpoint: 2 Cluster: 0x0000_001D Attribute 0x0000_0000 DataVersion: 3521853224 + [1653012107.334929][1030544:1030549] CHIP:TOO: device list: 1 entries + [1653012107.334973][1030544:1030549] CHIP:TOO: [1]: { + [1653012107.334997][1030544:1030549] CHIP:TOO: Type: 256 + [1653012107.335030][1030544:1030549] CHIP:TOO: Revision: 1 + [1653012107.335053][1030544:1030549] CHIP:TOO: } + """), + "code": + 0, +}, { + "cmd": + "/usr/local/bin/chip-tool descriptor read server-list 1234 1", + "resp": + textwrap.dedent(""" + [1653012222.678497][1030572:1030577] CHIP:EM: Removed CHIP MessageCounter:8097092 from RetransTable on exchange 42291i + [1653012222.678733][1030572:1030577] CHIP:DMG: ReportDataMessage = + [1653012222.678762][1030572:1030577] CHIP:DMG: { + [1653012222.678783][1030572:1030577] CHIP:DMG: AttributeReportIBs = + [1653012222.678815][1030572:1030577] CHIP:DMG: [ + [1653012222.678839][1030572:1030577] CHIP:DMG: AttributeReportIB = + [1653012222.678884][1030572:1030577] CHIP:DMG: { + [1653012222.678918][1030572:1030577] CHIP:DMG: AttributeDataIB = + [1653012222.678954][1030572:1030577] CHIP:DMG: { + [1653012222.678999][1030572:1030577] CHIP:DMG: DataVersion = 0xc7508647, + [1653012222.679037][1030572:1030577] CHIP:DMG: AttributePathIB = + [1653012222.679065][1030572:1030577] CHIP:DMG: { + [1653012222.679095][1030572:1030577] CHIP:DMG: Endpoint = 0x1, + [1653012222.679124][1030572:1030577] CHIP:DMG: Cluster = 0x1d, + [1653012222.679161][1030572:1030577] CHIP:DMG: Attribute = 0x0000_0001, + [1653012222.679200][1030572:1030577] CHIP:DMG: } + [1653012222.679236][1030572:1030577] CHIP:DMG: + [1653012222.679278][1030572:1030577] CHIP:DMG: Data = [ + [1653012222.679309][1030572:1030577] CHIP:DMG: + [1653012222.679352][1030572:1030577] CHIP:DMG: ], + [1653012222.679394][1030572:1030577] CHIP:DMG: }, + [1653012222.679428][1030572:1030577] CHIP:DMG: + [1653012222.679463][1030572:1030577] CHIP:DMG: }, + [1653012222.679510][1030572:1030577] CHIP:DMG: + [1653012222.679535][1030572:1030577] CHIP:DMG: AttributeReportIB = + [1653012222.679575][1030572:1030577] CHIP:DMG: { + [1653012222.679601][1030572:1030577] CHIP:DMG: AttributeDataIB = + [1653012222.679640][1030572:1030577] CHIP:DMG: { + [1653012222.679700][1030572:1030577] CHIP:DMG: DataVersion = 0xc7508647, + [1653012222.679732][1030572:1030577] CHIP:DMG: AttributePathIB = + [1653012222.679772][1030572:1030577] CHIP:DMG: { + [1653012222.679815][1030572:1030577] CHIP:DMG: Endpoint = 0x1, + [1653012222.679848][1030572:1030577] CHIP:DMG: Cluster = 0x1d, + [1653012222.679891][1030572:1030577] CHIP:DMG: Attribute = 0x0000_0001, + [1653012222.679943][1030572:1030577] CHIP:DMG: ListIndex = Null, + [1653012222.679969][1030572:1030577] CHIP:DMG: } + [1653012222.680011][1030572:1030577] CHIP:DMG: + [1653012222.680056][1030572:1030577] CHIP:DMG: Data = 3, + [1653012222.680088][1030572:1030577] CHIP:DMG: }, + [1653012222.680128][1030572:1030577] CHIP:DMG: + [1653012222.680153][1030572:1030577] CHIP:DMG: }, + [1653012222.680199][1030572:1030577] CHIP:DMG: + [1653012222.681618][1030572:1030577] CHIP:DMG: AttributeReportIB = + [1653012222.681650][1030572:1030577] CHIP:DMG: { + [1653012222.681684][1030572:1030577] CHIP:DMG: AttributeDataIB = + [1653012222.681712][1030572:1030577] CHIP:DMG: { + [1653012222.681751][1030572:1030577] CHIP:DMG: DataVersion = 0xc7508647, + [1653012222.681785][1030572:1030577] CHIP:DMG: AttributePathIB = + [1653012222.681828][1030572:1030577] CHIP:DMG: { + [1653012222.681867][1030572:1030577] CHIP:DMG: Endpoint = 0x1, + [1653012222.681903][1030572:1030577] CHIP:DMG: Cluster = 0x1d, + [1653012222.681947][1030572:1030577] CHIP:DMG: Attribute = 0x0000_0001, + [1653012222.681988][1030572:1030577] CHIP:DMG: ListIndex = Null, + [1653012222.682020][1030572:1030577] CHIP:DMG: } + [1653012222.682066][1030572:1030577] CHIP:DMG: + [1653012222.682111][1030572:1030577] CHIP:DMG: Data = 6, + [1653012222.682152][1030572:1030577] CHIP:DMG: }, + [1653012222.682186][1030572:1030577] CHIP:DMG: + [1653012222.682222][1030572:1030577] CHIP:DMG: }, + [1653012222.682267][1030572:1030577] CHIP:DMG: + [1653012222.702070][1030572:1030577] CHIP:DMG: ], + [1653012222.702275][1030572:1030577] CHIP:DMG: + [1653012222.702301][1030572:1030577] CHIP:DMG: MoreChunkedMessages = true, + [1653012222.702326][1030572:1030577] CHIP:DMG: InteractionModelRevision = 1 + [1653012222.702349][1030572:1030577] CHIP:DMG: } + """), + "code": + 0, }, { "cmd": "echo 1234 > ~/.matter_sdk_version", "resp": "",