[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(