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 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/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/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/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..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,38 +80,40 @@ 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 | int] + """ Determines whether or not this message bus has the role of ot bus. """ 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.conneciton_args = dict() - for k1, v1 in config[k].items(): - definition.conneciton_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) + + mb_def = MessageBusDefinition(**json_obj) + if not hasattr(mb_def, "is_ot_bus"): + setattr(mb_def, "is_ot_bus", False) - if not hasattr(definition, "is_ot_bus"): - setattr(definition, "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..9aa33bd 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_managers.centralized_context_managers:_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 new file mode 100644 index 0000000..0a8a978 --- /dev/null +++ b/gridappsd-field-bus-lib/tests/test_create_messagebus_definition.py @@ -0,0 +1,212 @@ +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): + + 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 + 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