[Local Agent] Sync code to support user-defined plugin.
Change-Id: I2e10021565ec6dca608570c1231ccb2dca381670
diff --git a/README.md b/README.md
index 168e82d..c918f9f 100644
--- a/README.md
+++ b/README.md
@@ -57,3 +57,10 @@
2. On your host machine, activate the virtual environment where you installed the local agent, and start the local agent by simply running ```$ local-agent```.
3. Once you’ve started your local agent, you should see a prompt that asks you for the linking code. Paste the linking code you copied from the web app. The local agent should start the linking process, and it will also start the device detection process, which should detect your DUTs that are connected to your host machine.
+
+
+## How to extend your Matter device controllers in Gazoo Device Manager
+
+The device control library we're using in local agent is GDM, also known as [Gazoo Device Manager](https://github.com/google/gazoo-device), which is also a python package for interacting with Matter devices.
+To allow local agent to talk to your Matter devices, you must build and add your own Matter device controllers to GDM. Please follow the extension [Matter controller codelab in GDM](https://github.com/google/gazoo-device/tree/master/examples/example_matter_extension_package) to build and install.
+Once your controllers are installed in your virtual environment, fill the names of your extension packages in ```~/.config/google/local_agent_config.ini```. See ```example_config.ini``` for references.
diff --git a/example_config.ini b/example_config.ini
index c369c81..d316118 100644
--- a/example_config.ini
+++ b/example_config.ini
@@ -12,7 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-[LocalAgentConfig]
+[ServerConfig]
AMS_HOST = localhost
AMS_PORT = 8080
AMS_SCHEME = http
+
+[MatterDeviceControllerPackages]
+example_matter_extension_package
diff --git a/local_agent/local_agent.py b/local_agent/local_agent.py
index cc97bf6..b029faf 100644
--- a/local_agent/local_agent.py
+++ b/local_agent/local_agent.py
@@ -17,6 +17,7 @@
from concurrent import futures
import configparser
import enum
+import importlib
import json
import os
import shutil
@@ -25,7 +26,7 @@
import threading
import time
import traceback
-from typing import Any, Dict, Optional, Tuple
+from typing import Any, Dict, List, Optional, Tuple
import gazoo_device
@@ -48,11 +49,12 @@
'~/.config/google/local_agent_config.ini')
_START_TEST_SUITE_METHOD = 'startTestSuite'
_END_TEST_SUITE_METHOD = 'endTestSuite'
-_USER_CONFIG_ROOT_KEY = 'LocalAgentConfig'
+_USER_CONFIG_ROOT_KEY = 'ServerConfig'
_USER_CONFIG_AMS_HOST = 'AMS_HOST'
_USER_CONFIG_AMS_PORT = 'AMS_PORT'
_USER_CONFIG_AMS_SCHEME = 'AMS_SCHEME'
_USER_CONFIG_ARTIFACTS_DIR = 'ARTIFACTS_DIR'
+_USER_CONFIG_MATTER_DEVICE_CONTROLLERS = 'MatterDeviceControllerPackages'
# ======================================================================= #
@@ -524,7 +526,7 @@
self._termination_event.set()
-def read_config() -> Dict[str, Any]:
+def read_config() -> Tuple[Dict[str, Any], List[Tuple[str, str]]]:
"""Reads user data from configuration file.
The config file should be in YAML format. User can specify the path to
@@ -538,7 +540,7 @@
RuntimeError: If unable to parse the config file as YAML.
Returns:
- User configuration data.
+ User configuration data for AMS and extension Matter device controllers.
"""
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user_config', type=str, required=False,
@@ -550,7 +552,7 @@
if not os.path.exists(args.user_config):
return {}
- config = configparser.ConfigParser()
+ config = configparser.ConfigParser(allow_no_value=True)
config.read(args.user_config)
if _USER_CONFIG_ROOT_KEY not in config:
@@ -558,18 +560,38 @@
f'Invalid config file, no section {_USER_CONFIG_ROOT_KEY}.'
'Please refer to example_config.ini for reference.')
- return config[_USER_CONFIG_ROOT_KEY]
+ matter_controllers_config = (
+ config.items(_USER_CONFIG_MATTER_DEVICE_CONTROLLERS)
+ if _USER_CONFIG_MATTER_DEVICE_CONTROLLERS in config else [])
+
+
+ return config[_USER_CONFIG_ROOT_KEY], matter_controllers_config
+
+
+def register_extension_controllers(
+ matter_controllers_config: List[Tuple[str, str]]) -> None:
+ """Registers partner extension Matter device controllers in GDM.
+
+ Args:
+ matter_controllers_config: List of controllers to be registered.
+ """
+ for controller_package_name, _ in matter_controllers_config:
+ controller = importlib.import_module(controller_package_name)
+ gazoo_device.register(controller)
def main() -> None:
"""Main entry of Local Agent."""
- user_config = read_config()
+ user_config, matter_controllers_config = read_config()
+
ams_host = user_config.get(_USER_CONFIG_AMS_HOST)
ams_port = user_config.get(_USER_CONFIG_AMS_PORT)
ams_scheme = user_config.get(_USER_CONFIG_AMS_SCHEME)
artifacts_dir = (
user_config.get(_USER_CONFIG_ARTIFACTS_DIR, DEFAULT_ARTIFACTS_DIR))
+ register_extension_controllers(matter_controllers_config)
+
client = ams_client.AmsClient(host=ams_host,
port=ams_port,
scheme=ams_scheme)
diff --git a/local_agent/tests/README.md b/local_agent/tests/README.md
index 72f506f..c91aebd 100644
--- a/local_agent/tests/README.md
+++ b/local_agent/tests/README.md
@@ -33,7 +33,7 @@
1. Backend setup: If you're an internal Googler or tester, please contact the local agent author for the backend deployment; Otherwise, you won't be able to deploy the backend.
-2. Edit the local agent config file: filling the ```AMS_HOST``` and ```AMS_PORT``` fields with the AMS configurations. Contact the local agent author if you're not sure about the backend configurations. See ```example_config.yaml``` for references.
+2. Edit the local agent config file: filling the ```AMS_HOST``` and ```AMS_PORT``` fields with the AMS configurations. Contact the local agent author if you're not sure about the backend configurations. See ```example_config.ini``` for references.
3. Activate the virtual environment.
@@ -51,7 +51,7 @@
```
local-agent -u [config file location]
```
- or just put the config under ```~/.config/google/local_agent_config.yaml```, then start the process parameterlessly:
+ or just put the config under ```~/.config/google/local_agent_config.ini```, then start the process parameterlessly:
```
local-agent
```
diff --git a/local_agent/tests/unit_tests/test_local_agent.py b/local_agent/tests/unit_tests/test_local_agent.py
index eba1454..3b796a3 100644
--- a/local_agent/tests/unit_tests/test_local_agent.py
+++ b/local_agent/tests/unit_tests/test_local_agent.py
@@ -43,6 +43,7 @@
_START_TEST_SUITE = 'startTestSuite'
_END_TEST_SUITE = 'endTestSuite'
_LOCK_DEVICE = 'setLock'
+_FAKE_CONTROLLER_PACKAGE = 'FAKE_CONTROLLER_PACKAGE'
#############################################################################
@@ -502,13 +503,18 @@
self.assertEqual(1, mock_ams_client_upload.call_count)
@mock.patch.object(local_agent, 'LocalAgentProcess')
+ @mock.patch.object(local_agent, 'register_extension_controllers')
@mock.patch.object(local_agent, 'read_config')
- def test_12_main_entry(self, mock_read, mock_proc):
+ def test_12_main_entry(self, mock_read, mock_register, mock_proc):
"""Verifies local agent main entry on success."""
+ mock_read.return_value = {}, []
+
local_agent.main()
- self.assertEqual(1, mock_read.call_count)
- self.assertEqual(1, mock_proc.call_count)
- self.assertEqual(1, mock_proc.return_value.run.call_count)
+
+ mock_read.assert_called_once()
+ mock_register.assert_called_once()
+ mock_proc.assert_called_once()
+ mock_proc.return_value.run.assert_called_once()
@mock.patch.object(translation_layer.TranslationLayer,'is_rpc_timeout')
@mock.patch.object(ams_client.AmsClient, 'send_rpc_response')
@@ -644,6 +650,18 @@
mock_future.exception.assert_called_once()
mock_logger.error.assert_called_once()
+ @mock.patch.object(local_agent, 'gazoo_device')
+ @mock.patch.object(local_agent, 'importlib')
+ def test_17_register_extension_controllers_on_success(
+ self, mock_importlib, mock_gazoo_device):
+ """Verifies register_extension_controllers on success."""
+ matter_controllers_config = [(_FAKE_CONTROLLER_PACKAGE, '')]
+
+ local_agent.register_extension_controllers(matter_controllers_config)
+
+ mock_importlib.import_module.assert_called_once()
+ mock_gazoo_device.register.assert_called_once()
+
if __name__ == '__main__':
unittest.main(failfast=True)