From eda8dee5531302898fc9a0772bf96ef6fddb1b3e Mon Sep 17 00:00:00 2001 From: "C. Allwardt" <3979063+craig8@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:07:09 -0800 Subject: [PATCH 1/5] Fixed spelling of connection --- .../field_interface/context_managers/utils.py | 2 +- .../field_interface/gridappsd_field_bus.py | 6 +++--- .../gridappsd_field_bus/field_interface/interfaces.py | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/context_managers/utils.py b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/context_managers/utils.py index 6c10dd8..e5c85ba 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/context_managers/utils.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/context_managers/utils.py @@ -18,6 +18,6 @@ def get_message_bus_definition(area_id: str) -> MessageBusDefinition: bus = MessageBusDefinition(id=area_id, is_ot_bus=True, connection_type="GRIDAPPSD_TYPE_GRIDAPPSD", - conneciton_args=connection_args) + connection_args=connection_args) return bus diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/gridappsd_field_bus.py b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/gridappsd_field_bus.py index e14e0b1..db4a8a2 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/gridappsd_field_bus.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/gridappsd_field_bus.py @@ -10,9 +10,9 @@ def __init__(self, definition: MessageBusDefinition): super(GridAPPSDMessageBus, self).__init__(definition) self._id = definition.id - self._user = definition.conneciton_args["GRIDAPPSD_USER"] - self._password = definition.conneciton_args["GRIDAPPSD_PASSWORD"] - self._address = definition.conneciton_args["GRIDAPPSD_ADDRESS"] + self._user = definition.connection_args["GRIDAPPSD_USER"] + self._password = definition.connection_args["GRIDAPPSD_PASSWORD"] + self._address = definition.connection_args["GRIDAPPSD_ADDRESS"] self.gridappsd_obj = None diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/interfaces.py b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/interfaces.py index 237bac3..56d9a65 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/interfaces.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/interfaces.py @@ -79,7 +79,8 @@ class MessageBusDefinition: """ connection_args allows dynamic key/value paired strings to be added to allow connections. """ - conneciton_args: Dict[str, str] + connection_args: Dict[str, str] + """ Determines whether or not this message bus has the role of ot bus. """ @@ -101,9 +102,9 @@ def load(config_file) -> MessageBusDefinition: config[required[2]]) for k in config: if k == "connection_args": - definition.conneciton_args = dict() + definition.connection_args = dict() for k1, v1 in config[k].items(): - definition.conneciton_args[k1] = v1 + definition.connection_args[k1] = v1 else: setattr(definition, k, config[k]) From da23f1fc2991489885247538270b8fa505326d77 Mon Sep 17 00:00:00 2001 From: "C. Allwardt" <3979063+craig8@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:08:32 -0800 Subject: [PATCH 2/5] Added code-workspace to gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e251546..b601ea7 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,4 @@ ENV/ # pytest cache .pytest_cache +/gridappsd-python.code-workspace From 555c87fdc6f374f5af344eddea97591027de286b Mon Sep 17 00:00:00 2001 From: "C. Allwardt" <3979063+craig8@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:30:07 -0800 Subject: [PATCH 3/5] Add tests for current dataclass messagebus definitions. --- .../gridappsd_field_bus/__init__.py | 9 +- .../test_create_messagebus_definition.py | 156 ++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/__init__.py b/gridappsd-field-bus-lib/gridappsd_field_bus/__init__.py index 158a6a5..623136e 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/__init__.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/__init__.py @@ -1,2 +1,7 @@ -from gridappsd_field_bus.field_interface.interfaces import FieldMessageBus -from gridappsd_field_bus.field_interface.interfaces import MessageBusDefinition \ No newline at end of file +from gridappsd_field_bus.field_interface.interfaces import ( + MessageBusDefinitions, + MessageBusDefinition, + FieldMessageBus, + FieldProtocol, + DeviceFieldInterface +) \ No newline at end of file diff --git a/gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py b/gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py new file mode 100644 index 0000000..3161519 --- /dev/null +++ b/gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py @@ -0,0 +1,156 @@ +import unittest +from unittest.mock import patch, mock_open, MagicMock + +import yaml + +from gridappsd_field_bus.field_interface.interfaces import FieldMessageBus, MessageBusDefinition, MessageBusDefinitions + +class MockDeviceFieldInterface: + pass + +class TestFieldMessageBus(unittest.TestCase): + + def setUp(self): + self.message_bus_definition = MessageBusDefinition(id="test_id", connection_type="test_type", connection_args={"arg1": "value1"}) + self.field_message_bus = FieldMessageBus(config=self.message_bus_definition) + + def test_id(self): + self.assertEqual(self.field_message_bus.id, "test_id") + + def test_is_ot_bus(self): + self.assertFalse(self.field_message_bus.is_ot_bus) + + def test_add_device(self): + device = MockDeviceFieldInterface() + self.field_message_bus.add_device = MagicMock() + self.field_message_bus.add_device(device) + self.field_message_bus.add_device.assert_called_with(device) + + def test_disconnect_device(self): + device = MockDeviceFieldInterface() + self.field_message_bus.add_device = MagicMock() + self.field_message_bus.disconnect_device = MagicMock() + self.field_message_bus.add_device(device) + self.field_message_bus.disconnect_device(device) + self.field_message_bus.disconnect_device.assert_called_with(device) + +class TestMessageBusDefinitionSingle(unittest.TestCase): + + @patch("builtins.open", new_callable=mock_open, read_data=""" +connections: + id: test_id + connection_type: test_type + connection_args: + arg1: value1 + arg2: value2 +""") + @patch("yaml.load") + def test_load_message_bus_definition(self, mock_yaml_load, mock_file): + mock_yaml_load.return_value = { + 'connections': { + 'id': 'test_id', + 'connection_type': 'test_type', + 'connection_args': { + 'arg1': 'value1', + 'arg2': 'value2' + } + } + } + + config_file = "dummy_path.yaml" + config = yaml.load(open(config_file), Loader=yaml.FullLoader)['connections'] + + required = ["id", "connection_type", "connection_args"] + for k in required: + if k not in config: + raise ValueError(f"Missing keys for connection {k}") + + definition = MessageBusDefinition(config[required[0]], config[required[1]], config[required[2]]) + for k in config: + if k == "connection_args": + definition.connection_args = dict() + for k1, v1 in config[k].items(): + definition.connection_args[k1] = v1 + else: + setattr(definition, k, config[k]) + + if not hasattr(definition, "is_ot_bus"): + setattr(definition, "is_ot_bus", False) + + self.assertEqual(definition.id, "test_id") + self.assertEqual(definition.connection_type, "test_type") + self.assertEqual(definition.connection_args, {"arg1": "value1", "arg2": "value2"}) + self.assertFalse(definition.is_ot_bus) + +class TestMessageBusDefinition(unittest.TestCase): + + @patch("builtins.open", new_callable=mock_open, read_data=""" +connections: + - id: test_id_1 + connection_type: test_type_1 + connection_args: + arg1: value1 + arg2: value2 + - id: test_id_2 + connection_type: test_type_2 + connection_args: + arg3: value3 + arg4: value4 +""") + @patch("yaml.load") + def test_load_message_bus_definition_multiple_connections(self, mock_yaml_load, mock_file): + mock_yaml_load.return_value = { + 'connections': [ + { + 'id': 'test_id_1', + 'connection_type': 'test_type_1', + 'connection_args': { + 'arg1': 'value1', + 'arg2': 'value2' + } + }, + { + 'id': 'test_id_2', + 'connection_type': 'test_type_2', + 'connection_args': { + 'arg3': 'value3', + 'arg4': 'value4' + } + } + ] + } + + config_file = "dummy_path.yaml" + configs = yaml.load(open(config_file), Loader=yaml.FullLoader)['connections'] + + for config in configs: + required = ["id", "connection_type", "connection_args"] + for k in required: + if k not in config: + raise ValueError(f"Missing keys for connection {k}") + + definition = MessageBusDefinition(config[required[0]], config[required[1]], config[required[2]]) + for k in config: + if k == "connection_args": + definition.connection_args = dict() + for k1, v1 in config[k].items(): + definition.connection_args[k1] = v1 + else: + setattr(definition, k, config[k]) + + if not hasattr(definition, "is_ot_bus"): + setattr(definition, "is_ot_bus", False) + + if definition.id == "test_id_1": + self.assertEqual(definition.id, "test_id_1") + self.assertEqual(definition.connection_type, "test_type_1") + self.assertEqual(definition.connection_args, {"arg1": "value1", "arg2": "value2"}) + self.assertFalse(definition.is_ot_bus) + elif definition.id == "test_id_2": + self.assertEqual(definition.id, "test_id_2") + self.assertEqual(definition.connection_type, "test_type_2") + self.assertEqual(definition.connection_args, {"arg3": "value3", "arg4": "value4"}) + self.assertFalse(definition.is_ot_bus) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 83ad8364e32830def1e5a761063c6d0e0de0df84 Mon Sep 17 00:00:00 2001 From: "C. Allwardt" <3979063+craig8@users.noreply.github.com> Date: Sun, 2 Feb 2025 19:35:34 -0800 Subject: [PATCH 4/5] Exposed command line for starting the field_proxy_forwarder from tool --- .../field_interface/field_proxy_forwarder.py | 4 +- .../field_interface/interfaces.py | 44 ++++++++------- .../gridappsd_field_bus/forwarder.py | 28 ++++++++++ gridappsd-field-bus-lib/pyproject.toml | 11 ++-- .../test_create_messagebus_definition.py | 56 +++++++++++++++++++ 5 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 gridappsd-field-bus-lib/gridappsd_field_bus/forwarder.py diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/field_proxy_forwarder.py b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/field_proxy_forwarder.py index 05628cf..928141f 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/field_proxy_forwarder.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/field_proxy_forwarder.py @@ -7,7 +7,7 @@ REQUEST_FIELD = ".".join((topics.PROCESS_PREFIX, "request.field")) -class FieldListener(): +class FieldListener: def __init__(self, ot_connection: GridAPPSD, proxy_connection: stomp.Connection): self.ot_connection = ot_connection @@ -37,7 +37,7 @@ def on_message(self, headers, message): except Exception as e: print(f"Error processing message: {e}") -class FieldProxyForwarder(): +class FieldProxyForwarder: """ FieldProxyForwarder acts as a bridge between field bus and OT bus when direct connection is not possible. diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/interfaces.py b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/interfaces.py index 56d9a65..c7d0b39 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/interfaces.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/interfaces.py @@ -7,7 +7,7 @@ import logging from os import PathLike from pathlib import Path -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, Any import yaml @@ -31,6 +31,7 @@ class ConnectionType(Enum): # CONNECTION_TYPE_WS = "WS" # CONNECTION_TYPE_HTTP = "HTTP" # CONNECTION_TYPE_TCP = "TCP" + CONNECTION_TYPE = "STOMP" CONNECTION_TYPE_GRIDAPPSD = "CONNECTION_TYPE_GRIDAPPSD" @@ -79,7 +80,7 @@ class MessageBusDefinition: """ connection_args allows dynamic key/value paired strings to be added to allow connections. """ - connection_args: Dict[str, str] + connection_args: Dict[str, str | int] """ Determines whether or not this message bus has the role of ot bus. @@ -87,31 +88,32 @@ class MessageBusDefinition: is_ot_bus: bool = False @staticmethod - def load(config_file) -> MessageBusDefinition: - """ - - """ - config = yaml.load(open(config_file), Loader=yaml.FullLoader)['connections'] - + def __validate_loader__(json_obj: dict[str, Any]) -> bool: required = ["id", "connection_type", "connection_args"] for k in required: - if k not in config: + if k not in json_obj: raise ValueError(f"Missing keys for connection {k}") - definition = MessageBusDefinition(config[required[0]], config[required[1]], - config[required[2]]) - for k in config: - if k == "connection_args": - definition.connection_args = dict() - for k1, v1 in config[k].items(): - definition.connection_args[k1] = v1 - else: - setattr(definition, k, config[k]) + return True + + @staticmethod + def load_from_json(json_obj: dict[str, str | dict]) -> MessageBusDefinition: + MessageBusDefinition.__validate_loader__(json_obj) - if not hasattr(definition, "is_ot_bus"): - setattr(definition, "is_ot_bus", False) + mb_def = MessageBusDefinition(**json_obj) + if not hasattr(mb_def, "is_ot_bus"): + setattr(mb_def, "is_ot_bus", False) + + return mb_def + + @staticmethod + def load(config_file) -> MessageBusDefinition: + """ + Load a single message bus definition from a YAML file. + """ + config = yaml.load(open(config_file), Loader=yaml.FullLoader)['connections'] - return definition + return MessageBusDefinition.load_from_json(config) class FieldMessageBus: diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/forwarder.py b/gridappsd-field-bus-lib/gridappsd_field_bus/forwarder.py new file mode 100644 index 0000000..c744c9e --- /dev/null +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/forwarder.py @@ -0,0 +1,28 @@ +import time + +import click +import yaml +import os + +from gridappsd_field_bus import MessageBusDefinition +from gridappsd_field_bus.field_interface.field_proxy_forwarder import FieldProxyForwarder + + +@click.command() +@click.option('--username', required=True, help='Username for the connection.') +@click.option('--password', required=True, help='Password for the connection.') +@click.option('--connection_url', required=True, help='Connection URL.') +def start_forwarder(username, password, connection_url): + """Start the field proxy forwarder with either a YAML configuration or cmd-line arguments.""" + + # Use command-line arguments + click.echo(f"Using command line arguments: {username}, {password}, {connection_url}") + + proxy_forwarder = FieldProxyForwarder(username, password, connection_url) + + time.sleep(0.1) + + + +if __name__ == '__main__': + start_forwarder() \ No newline at end of file diff --git a/gridappsd-field-bus-lib/pyproject.toml b/gridappsd-field-bus-lib/pyproject.toml index 6fbc8dc..8dd13d4 100644 --- a/gridappsd-field-bus-lib/pyproject.toml +++ b/gridappsd-field-bus-lib/pyproject.toml @@ -21,15 +21,18 @@ packages = [ { include = 'gridappsd_field_bus'} ] -[tool.poetry.scripts] -# Add things in the form -# myscript = 'my_package:main' -context_manager = 'gridappsd.field_interface.context_manager:_main' [tool.poetry.dependencies] python = ">=3.10,<4.0" gridappsd-python = { path="../gridappsd-python-lib", develop = true} cim-graph = ">=0.1.1a0" +click = "^8.1.8" + +[tool.poetry.scripts] +# Add things in the form +# myscript = 'my_package:main' +start-field-bus-forwarder = 'gridappsd_field_bus.forwarder:start_forwarder' +context_manager = 'gridappsd_field_bus.field_interface.context_manager:_main' [tool.poetry.group.dev.dependencies] pytest = "^8.3.4" diff --git a/gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py b/gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py index 3161519..0a8a978 100644 --- a/gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py +++ b/gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py @@ -36,6 +36,62 @@ def test_disconnect_device(self): class TestMessageBusDefinitionSingle(unittest.TestCase): + def test_load_from_json_missing_keys(self): + # Missing "connection_type" + json_obj_missing_connection_type = { + "id": "test_id", + "connection_args": { + "arg1": "value1", + "arg2": "value2" + } + } + with self.assertRaises(ValueError) as context: + MessageBusDefinition.load_from_json(json_obj_missing_connection_type) + self.assertTrue("Missing keys for connection connection_type" in str(context.exception)) + + # Missing "id" + json_obj_missing_id = { + "connection_type": "test_type", + "connection_args": { + "arg1": "value1", + "arg2": "value2" + } + } + with self.assertRaises(ValueError) as context: + MessageBusDefinition.load_from_json(json_obj_missing_id) + self.assertTrue("Missing keys for connection id" in str(context.exception)) + + # Missing "connection_args" + json_obj_missing_connection_args = { + "id": "test_id", + "connection_type": "test_type" + } + with self.assertRaises(ValueError) as context: + MessageBusDefinition.load_from_json(json_obj_missing_connection_args) + self.assertTrue("Missing keys for connection connection_args" in str(context.exception)) + + def test_load_from_json(self): + json_obj = { + "id": "test_id", + "connection_type": "test_type", + "connection_args": { + "arg1": "value1", + "arg2": "value2" + } + } + + definition = MessageBusDefinition.load_from_json(json_obj) + + self.assertEqual(definition.id, "test_id") + self.assertEqual(definition.connection_type, "test_type") + self.assertEqual(definition.connection_args, {"arg1": "value1", "arg2": "value2"}) + self.assertFalse(definition.is_ot_bus) + + json_obj["is_ot_bus"] = True + definition = MessageBusDefinition.load_from_json(json_obj) + self.assertTrue(definition.is_ot_bus) + + @patch("builtins.open", new_callable=mock_open, read_data=""" connections: id: test_id From 927516963226ee6fa94091cbb28ab978152b2f59 Mon Sep 17 00:00:00 2001 From: Craig <3979063+craig8@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:47:41 -0800 Subject: [PATCH 5/5] Update centralized context managers path --- gridappsd-field-bus-lib/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gridappsd-field-bus-lib/pyproject.toml b/gridappsd-field-bus-lib/pyproject.toml index 8dd13d4..9aa33bd 100644 --- a/gridappsd-field-bus-lib/pyproject.toml +++ b/gridappsd-field-bus-lib/pyproject.toml @@ -32,7 +32,7 @@ click = "^8.1.8" # Add things in the form # myscript = 'my_package:main' start-field-bus-forwarder = 'gridappsd_field_bus.forwarder:start_forwarder' -context_manager = 'gridappsd_field_bus.field_interface.context_manager:_main' +context_manager = 'gridappsd_field_bus.field_interface.context_managers.centralized_context_managers:_main' [tool.poetry.group.dev.dependencies] pytest = "^8.3.4"