[Local Agent] Sync go/nest-cl/288136 and go/nest-cl/288758 Change-Id: I533639aeb3b3475b58966716397c01efbc9e5555
diff --git a/local_agent/tests/unit_tests/test_command_handlers/test_dimmable_light.py b/local_agent/tests/unit_tests/test_command_handlers/test_dimmable_light.py new file mode 100644 index 0000000..495ede0 --- /dev/null +++ b/local_agent/tests/unit_tests/test_command_handlers/test_dimmable_light.py
@@ -0,0 +1,37 @@ +# 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 DimmableLight endpoint command handler.""" +import unittest +from unittest import mock + +from local_agent.translation_layer.command_handlers import dimmable_light + + +class DimmableLightCommandHandlerTest(unittest.TestCase): + """Unit tests for DimmableLightCommandHandler.""" + + def setUp(self): + super().setUp() + self.mock_dut = mock.Mock() + self.handler = dimmable_light.DimmableLightCommandHandler(self.mock_dut) + + def test_endpoint_initialization(self): + """Verifies if endpoint instance is initialized successfully.""" + self.assertEqual(self.mock_dut.dimmable_light, self.handler.endpoint) + + + +if __name__ == '__main__': + unittest.main(failfast=True)
diff --git a/local_agent/tests/unit_tests/test_gdm_manager.py b/local_agent/tests/unit_tests/test_gdm_manager.py index 784c30a..9255248 100644 --- a/local_agent/tests/unit_tests/test_gdm_manager.py +++ b/local_agent/tests/unit_tests/test_gdm_manager.py
@@ -13,6 +13,7 @@ # limitations under the License. """Unit tests for gdm_manager.""" +from parameterized import parameterized import unittest from unittest import mock @@ -35,7 +36,6 @@ 'persistent': {'device_type': 'nrf52849', 'serial_number': '1234'}, }, } -_FAKE_DEVICE_CAPABILITIES = ['flash_build', 'switchboard'] _FAKE_ERROR_MSG = 'error' ############################################################################## @@ -47,37 +47,44 @@ super().setUp() self.mgr = gdm_manager.GdmManager(mock.Mock()) - @mock.patch.object(gazoo_device.Manager, 'get_supported_device_capabilities') + @parameterized.expand([(True, ['pw_rpc_common']), (False, [])]) + @mock.patch.object(gazoo_device.Manager, 'create_and_close_device') @mock.patch.object(gazoo_device.Manager, 'is_device_connected') @mock.patch.object(gazoo_device.Manager, 'get_devices') @mock.patch.object(gazoo_device.Manager, 'detect') - def test_01_detect_devices( + def test_detect_devices( self, + has_capabilities_result, + expected_capability, mock_detect, mock_get_devices, mock_is_connect, - mock_get_capabilities): + mock_create_and_close_device): """Verifies detect_devices on success.""" mock_get_devices.return_value = _FAKE_DETECTION_RESULT mock_is_connect.side_effect = [True, False] - mock_get_capabilities.return_value = _FAKE_DEVICE_CAPABILITIES + fake_device = mock.Mock() + mock_create_and_close_device.return_value.iter.return_value = fake_device + mock_create_and_close_device.return_value.__enter__.return_value = fake_device + fake_device.matter_endpoints.get_supported_endpoints_and_clusters.return_value = {} + fake_device.has_capabilities.return_value = has_capabilities_result + device_statuses = self.mgr.detect_devices() + expected_statuses = [ { 'deviceId': 'efr32-3453', 'serialNumber': '000440173453', 'deviceType': 'efr32', - 'capabilities': ['flash_build', 'switchboard'], + 'capabilities': expected_capability, }, ] self.assertEqual(expected_statuses, device_statuses) mock_detect.assert_called_once_with( force_overwrite=True, log_directory='/tmp') - self.assertEqual(2, mock_is_connect.call_count) - self.assertEqual(1, mock_get_capabilities.call_count) @mock.patch.object(gazoo_device.Manager, 'create_device') - def test_02_create_devices_on_success(self, mock_create): + def test_create_devices_on_success(self, mock_create): """Verifies create_devices on success.""" identifiers = [_FAKE_DEVICE_ID, _FAKE_DEVICE_ID2] self.mgr.create_devices(identifiers) @@ -86,7 +93,7 @@ self.assertEqual(2, mock_create.call_count) @mock.patch.object(gazoo_device.Manager, 'is_device_connected') - def test_03_check_device_connected_on_success( + def test_check_device_connected_on_success( self, mock_is_device_connected): """Verifies check_device_connected succeeds when device connected.""" mock_is_device_connected.return_value = True @@ -94,7 +101,7 @@ self.assertEqual(1, mock_is_device_connected.call_count) @mock.patch.object(gazoo_device.Manager, 'is_device_connected') - def test_03_check_device_connected_raises_when_disconnected( + def test_check_device_connected_raises_when_disconnected( self, mock_is_device_connected): """Verifies check_device_connected raises when device disconnected.""" mock_is_device_connected.return_value = False @@ -103,7 +110,7 @@ self.assertEqual(1, mock_is_device_connected.call_count) @mock.patch.object(gazoo_device.Manager, 'is_device_connected') - def test_03_check_device_connected_raises_unexpected_error( + def test_check_device_connected_raises_unexpected_error( self, mock_is_device_connected): """ Verifies check_device_connected raises when device operation has error. @@ -116,7 +123,7 @@ @mock.patch.object(gazoo_device.Manager, 'get_open_device') @mock.patch.object(gazoo_device.Manager, 'get_open_device_names') - def test_04_get_device_instance_on_success( + def test_get_device_instance_on_success( self, mock_get_open_device_names, mock_get_open_device): """Verifies get_device_instance on success.""" mock_get_open_device_names.return_value = [_FAKE_DEVICE_ID] @@ -125,7 +132,7 @@ mock_get_open_device.assert_called_once_with(_FAKE_DEVICE_ID) @mock.patch.object(gazoo_device.Manager, 'get_open_device_names') - def test_04_get_device_instance_failure_because_device_not_open( + def test_get_device_instance_failure_because_device_not_open( self, mock_get_open_device_names): """Verifies get_device_instance on failure.""" mock_get_open_device_names.return_value = [] @@ -135,7 +142,7 @@ @mock.patch.object(gazoo_device.Manager, 'get_open_device') @mock.patch.object(gazoo_device.Manager, 'get_open_device_names') - def test_05_get_device_type_on_success( + def test_get_device_type_on_success( self, mock_get_names, mock_get_device): """Verifies _get_device_type on success.""" mock_get_names.return_value = {_FAKE_DEVICE_ID,} @@ -146,7 +153,7 @@ self.assertEqual(1, mock_get_device.call_count) @mock.patch.object(gazoo_device.Manager, 'get_open_device_names') - def test_05_get_device_type_on_failure_not_open(self, mock_get_names): + def test_get_device_type_on_failure_not_open(self, mock_get_names): """Verifies _get_device_type on failure not open.""" mock_get_names.return_value = [] error_msg = f'{_FAKE_DEVICE_ID} is not open' @@ -154,7 +161,7 @@ self.mgr.get_device_type(_FAKE_DEVICE_ID) @mock.patch.object(gazoo_device.Manager, 'close_open_devices') - def test_06_close_open_devices_on_success(self, mock_close_open_devices): + def test_close_open_devices_on_success(self, mock_close_open_devices): """Verifies close_open_devices on success.""" self.mgr.close_open_devices() self.assertEqual(1, mock_close_open_devices.call_count)
diff --git a/local_agent/translation_layer/command_handlers/color_temperature_light.py b/local_agent/translation_layer/command_handlers/color_temperature_light.py index 7c07c6e..a1e92c8 100644 --- a/local_agent/translation_layer/command_handlers/color_temperature_light.py +++ b/local_agent/translation_layer/command_handlers/color_temperature_light.py
@@ -17,13 +17,9 @@ from gazoo_device import errors -from local_agent import logger as logger_module from local_agent.translation_layer.command_handlers import base from local_agent.translation_layer.command_handlers.cluster_handlers import color - -logger = logger_module.get_logger() - ENDPOINT_CAPABILITY = 'color_temperature_light'
diff --git a/local_agent/translation_layer/command_handlers/dimmable_light.py b/local_agent/translation_layer/command_handlers/dimmable_light.py new file mode 100644 index 0000000..4e55a40 --- /dev/null +++ b/local_agent/translation_layer/command_handlers/dimmable_light.py
@@ -0,0 +1,37 @@ +# 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. + +"""Module for DimmableLight endpoint command handler.""" +from typing import Any, Dict + +from gazoo_device import errors + +from local_agent.translation_layer.command_handlers import base +from local_agent.translation_layer.command_handlers.cluster_handlers import level +from local_agent.translation_layer.command_handlers.cluster_handlers import on_off + +ENDPOINT_CAPABILITY = 'dimmable_light' + + +class DimmableLightCommandHandler( + base.BaseCommandHandler, level.LevelControlHandler, on_off.OnOffHandler): + """Command handler for DimmableLight endpoint.""" + + SUPPORTED_METHODS = ( + level.LevelControlHandler.SUPPORTED_METHODS | + on_off.OnOffHandler.SUPPORTED_METHODS) + + def __init__(self, dut: Any) -> None: + super().__init__(dut) + self.endpoint = self.dut.dimmable_light
diff --git a/local_agent/translation_layer/command_handlers/handler_registry.py b/local_agent/translation_layer/command_handlers/handler_registry.py index b4ece37..f5dd7eb 100644 --- a/local_agent/translation_layer/command_handlers/handler_registry.py +++ b/local_agent/translation_layer/command_handlers/handler_registry.py
@@ -21,16 +21,16 @@ from local_agent.translation_layer.command_handlers import color_temperature_light from local_agent.translation_layer.command_handlers import common -from local_agent.translation_layer.command_handlers import on_off_light +from local_agent.translation_layer.command_handlers import dimmable_light from local_agent.translation_layer.command_handlers import door_lock +from local_agent.translation_layer.command_handlers import on_off_light -# GDM capability -> Matter endpoint command handlers -# TODO(b/216406955): Make command handler mapping dynamic generated GDM_CAPABILITIES_TO_COMMAND_HANDLERS = immutabledict.immutabledict({ color_temperature_light.ENDPOINT_CAPABILITY: color_temperature_light.ColorTemperatureLightCommandHandler, common.PWRPC_COMMON_CAPABILITY: common.CommonCommandHandler, + dimmable_light.ENDPOINT_CAPABILITY: dimmable_light.DimmableLightCommandHandler, + door_lock.ENDPOINT_CAPABILITY: door_lock.DoorLockCommandHandler, on_off_light.ENDPOINT_CAPABILITY: on_off_light.OnOffLightCommandHandler, - door_lock.ENDPOINT_CAPABILITY: door_lock.DoorLockCommandHandler })
diff --git a/local_agent/translation_layer/command_handlers/on_off_light.py b/local_agent/translation_layer/command_handlers/on_off_light.py index 02dbbac..859f325 100644 --- a/local_agent/translation_layer/command_handlers/on_off_light.py +++ b/local_agent/translation_layer/command_handlers/on_off_light.py
@@ -17,14 +17,10 @@ from gazoo_device import errors -from local_agent import logger as logger_module from local_agent.translation_layer.command_handlers import base from local_agent.translation_layer.command_handlers.cluster_handlers import level from local_agent.translation_layer.command_handlers.cluster_handlers import on_off - -logger = logger_module.get_logger() - ENDPOINT_CAPABILITY = 'on_off_light'
diff --git a/local_agent/translation_layer/gdm_manager.py b/local_agent/translation_layer/gdm_manager.py index 74b3c96..cfa6dcb 100644 --- a/local_agent/translation_layer/gdm_manager.py +++ b/local_agent/translation_layer/gdm_manager.py
@@ -21,6 +21,7 @@ from local_agent import errors as agent_errors from local_agent import logger as logger_module +from local_agent.translation_layer.command_handlers import common logger = logger_module.get_logger() @@ -40,6 +41,10 @@ self._first_detection = True self._update_handlers_cls_map = update_handlers_cls_map_fn + # Caching device information to avoid race condition when doing device + # communication multiple times. + self._connected_devices = {} # device id -> device information dict + def detect_devices(self) -> List[Dict[str, Any]]: """Detects connected devices. @@ -53,21 +58,43 @@ log_directory=_DEVICE_DETECTION_LOG_DIR) connected_devices = self._mgr.get_devices('all') for device_id, info in connected_devices.items(): + + # The device has been removed. if not self._mgr.is_device_connected(device_id): continue - serial_number = info['persistent']['serial_number'] - device_type = info['persistent']['device_type'] - capabilities = ( - self._mgr.get_supported_device_capabilities(device_type)) - device_dict = { - 'deviceId': device_id, - 'serialNumber': serial_number, - 'deviceType': device_type, - 'capabilities': capabilities, - } - devices.append(device_dict) - self._update_handlers_cls_map(device_type, capabilities) + + # The first time this device is detected. + if device_id not in self._connected_devices: + serial_number = info['persistent']['serial_number'] + device_type = info['persistent']['device_type'] + + # Retrieve the Matter endpoints and clusters in the first detection. + with self._mgr.create_and_close_device(device_id) as device: + matter_endpoints = device.matter_endpoints.get_supported_endpoints() + endpoint_clusters_mapping = device.matter_endpoints.get_supported_endpoints_and_clusters() + cluster_capabilities = list(set().union(*endpoint_clusters_mapping.values())) + + # Additional non-Matter capabilities. + if device.has_capabilities([common.PWRPC_COMMON_CAPABILITY]): + matter_endpoints.append(common.PWRPC_COMMON_CAPABILITY) + cluster_capabilities.append(common.PWRPC_COMMON_CAPABILITY) + + # Therefore, we only need to update the command handlers once. + self._update_handlers_cls_map(device_type, matter_endpoints) + + # Store the device information in cache. + device_info = { + 'deviceId': device_id, + 'serialNumber': serial_number, + 'deviceType': device_type, + 'capabilities': cluster_capabilities, + } + self._connected_devices[device_id] = device_info + + devices.append(self._connected_devices[device_id]) + self._first_detection = False + return devices def create_devices(