diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/__init__.py b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/__init__.py index f1e2141..af50827 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/__init__.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/__init__.py @@ -2,6 +2,6 @@ from gridappsd_field_bus.field_interface.agents.agents import (FeederAgent, DistributedAgent, CoordinatingAgent, SwitchAreaAgent, - SecondaryAreaAgent, SubstationAgent) + SecondaryAreaAgent, SubstationAgent, compute_req) __all__: List[str] = ["FeederAgent", "DistributedAgent", "CoordinatingAgent"] diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/agents.py b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/agents.py index 0bbd295..ac02eb5 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/agents.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/agents.py @@ -6,6 +6,13 @@ from datetime import datetime from typing import Dict +import time +import os +from functools import wraps +import sys +import inspect +import atexit + from cimgraph.databases import ConnectionParameters from cimgraph.databases.gridappsd import GridappsdConnection from cimgraph.models import FeederModel @@ -17,12 +24,18 @@ from gridappsd_field_bus.field_interface.gridappsd_field_bus import GridAPPSDMessageBus from gridappsd_field_bus.field_interface.interfaces import (FieldMessageBus, MessageBusDefinition, MessageBusFactory) + CIM_PROFILE = None IEC61970_301 = None cim = None _log = logging.getLogger(__name__) - +decorator_logger = logging.getLogger("decorator_logger") +decorator_logger.setLevel(logging.INFO) +file_handler = logging.FileHandler("compute_req_log.txt") # Log file name +formatter = logging.Formatter('[COMPUTE_REQ] %(asctime)s - %(message)s') +file_handler.setFormatter(formatter) +decorator_logger.addHandler(file_handler) def set_cim_profile(cim_profile: str, iec61970_301: int): global CIM_PROFILE @@ -41,6 +54,147 @@ class AgentRegistrationDetails: upstream_message_bus_id: FieldMessageBus.id downstream_message_bus_id: FieldMessageBus.id +@atexit.register +def call_counter_report(): + decorator_logger.info("Function call counts summary:") + for func_name, count in function_call_counts.items(): + decorator_logger.info(f"{func_name} was called {count} time(s)") + +@atexit.register +def message_size_report(): + decorator_logger.info("Total message size summary:") + for func_name, total_size in message_size_totals.items(): + decorator_logger.info(f"{func_name} total message size: {total_size} bytes") + +def compute_req(cls): + functions = [ + '__init__', + #'on_measurement', + 'on_upstream_message', + 'on_downstream_message', + 'on_request', + 'publish_upstream', + 'publish_downstream', + 'send_control_command' + ] + + def call_counter(func): + name = func.__qualname__ + + @wraps(func) + def wrapper(*args, **kwargs): + if args[0].agent_id+'.'+name not in function_call_counts: + function_call_counts[args[0].agent_id+'.'+name] = 0 + function_call_counts[args[0].agent_id+'.'+name] += 1 + #decorator_logger.info(f"{name} called {function_call_counts[name]} times") + return func(*args, **kwargs) + return wrapper + + def timed(func): + @wraps(func) + def wrapper(*args, **kwargs): + start = time.perf_counter() + result = func(*args, **kwargs) + end = time.perf_counter() + class_name = args[0].__class__.__name__ if args else "" + if func.__name__ == '__init__': + decorator_logger.info(f"{class_name}.{func.__name__}.{args[0].agent_id} took: {end - start:.6f} seconds") + return result + return wrapper + + def get_deep_size(func): + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + + def deep_size(obj, seen=None): + if seen is None: + seen = set() + obj_id = id(obj) + if obj_id in seen: + return 0 + seen.add(obj_id) + size = sys.getsizeof(obj) + if isinstance(obj, dict): + size += sum(deep_size(k, seen) + deep_size(v, seen) for k, v in obj.items()) + elif isinstance(obj, (list, tuple, set, frozenset)): + size += sum(deep_size(i, seen) for i in obj) + elif hasattr(obj, '__dict__'): + for attr_name, attr_value in vars(obj).items(): + if attr_name in ['feeder_area', 'switch_area', 'secondary_area']: + continue + size += deep_size(attr_value, seen) + elif hasattr(obj, '__slots__'): + size += sum(deep_size(getattr(obj, slot), seen) for slot in obj.__slots__ if hasattr(obj, slot)) + return size + + self = args[0] + obj_size = deep_size(self) + decorator_logger.info(f"{self.__class__.__name__}.{func.__name__}.{args[0].agent_id} size is: {obj_size} bytes") + + return result + return wrapper + + def get_graph_size(func): + @wraps(func) + def wrapper(*args, **kwargs): + self = args[0] + result = func(*args, **kwargs) + area_names = ['feeder_area', 'switch_area', 'secondary_area'] + area_found = False + for name in area_names: + area_dict = getattr(self, name, None) + if area_dict is not None and hasattr(area_dict, 'graph'): + graph_keys = [key.__name__ for key in list(area_dict.graph.keys())] + size = len(area_dict.graph.keys()) + decorator_logger.info(f"{self.__class__.__name__}.{func.__name__}.{args[0].agent_id} length of graph: {size}") + decorator_logger.info(f"{self.__class__.__name__}.{name}.{args[0].agent_id} graph keys: {graph_keys}") + area_found = True + break + + if not area_found: + decorator_logger.error(f"{class_name}.{func.__name__}.{args[0].agent_id} No area dictionary (feeder/switch/secondary) found in {self.__class__.__name__}") + return result + return wrapper + + def log_message_size(func): + name = func.__qualname__ + + @wraps(func) + def wrapper(*args, **kwargs): + sig = inspect.signature(func) + bound_args = sig.bind(*args, **kwargs) + bound_args.apply_defaults() + + if 'message' in bound_args.arguments: + msg = bound_args.arguments['message'] + size = sys.getsizeof(msg) + if args[0].agent_id+'.'+name not in message_size_totals: + message_size_totals[args[0].agent_id+'.'+name] = 0 + message_size_totals[args[0].agent_id+'.'+name] += size + + if 'differenceBuilder' in bound_args.arguments: + msg = bound_args.arguments['differenceBuilder'] + size = sys.getsizeof(msg) + if args[0].agent_id+'.'+name not in message_size_totals: + message_size_totals[args[0].agent_id+'.'+name] = 0 + message_size_totals[args[0].agent_id+'.'+name] += size + + return func(*args, **kwargs) + return wrapper + + # Decorate the relevant functions + for attr_name in functions: + if hasattr(cls, attr_name): + original_func = getattr(cls, attr_name) + if callable(original_func): + if attr_name == '__init__': + decorated = get_deep_size(get_graph_size(timed(original_func))) + else: + decorated = call_counter(log_message_size(timed(original_func))) + setattr(cls, attr_name, decorated) + + return cls class DistributedAgent: @@ -61,10 +215,7 @@ def __init__(self, self.simulation_id = simulation_id self.context = None - # TODO: Change params and connection to local connection - self.params = ConnectionParameters(cim_profile=CIM_PROFILE, iec61970_301=IEC61970_301) - - self.connection = GridappsdConnection(self.params) + self.connection = GridappsdConnection() self.connection.cim_profile = cim_profile self.app_id = agent_config['app_id'] @@ -79,14 +230,10 @@ def __init__(self, self.agent_area_dict = agent_area_dict if upstream_message_bus_def is not None: - if upstream_message_bus_def.is_ot_bus: - self.upstream_message_bus = MessageBusFactory.create(upstream_message_bus_def) - # else: - # self.upstream_message_bus = VolttronMessageBus(upstream_message_bus_def) - + self.upstream_message_bus = MessageBusFactory.create(upstream_message_bus_def) + if downstream_message_bus_def is not None: - if downstream_message_bus_def.is_ot_bus: - self.downstream_message_bus = MessageBusFactory.create(downstream_message_bus_def) + self.downstream_message_bus = MessageBusFactory.create(downstream_message_bus_def) if self.downstream_message_bus is None and self.upstream_message_bus is None: raise ValueError("Must have at least a downstream and/or upstream message bus specified") 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 928141f..7203f58 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 @@ -5,6 +5,13 @@ from gridappsd import GridAPPSD from gridappsd import topics +from cimgraph.databases import GridappsdConnection, BlazegraphConnection +from cimgraph.models import BusBranchModel, FeederModel + +import os +import cimgraph.utils as utils +import cimgraph.data_profile.cimhub_ufls as cim + REQUEST_FIELD = ".".join((topics.PROCESS_PREFIX, "request.field")) class FieldListener: @@ -16,7 +23,7 @@ def __init__(self, ot_connection: GridAPPSD, proxy_connection: stomp.Connection) def on_message(self, headers, message): "Receives messages coming from Proxy bus (e.g. ARTEMIS) and forwards to OT bus" try: - print(f"Received message at Proxy: {message}") + print(f"Received message at Proxy. destination: {headers['destination']}, message: {headers}") if headers["destination"] == topics.field_output_topic(): self.ot_connection.send(topics.field_output_topic(), message) @@ -29,8 +36,11 @@ def on_message(self, headers, message): request_type = request_data.get("request_type") if request_type == "get_context": response = self.ot_connection.get_response(headers["destination"],message) - self.proxy_connection.send(headers["reply_to"],response) - + self.proxy_connection.send(headers["reply-to"],response) + elif request_type == "start_publishing": + response = self.ot_connection.get_response(headers["destination"],message) + self.proxy_connection.send(headers["reply-to"],json.dumps(response)) + else: print(f"Unrecognized message received by Proxy: {message}") @@ -43,7 +53,7 @@ class FieldProxyForwarder: when direct connection is not possible. """ - def __init__(self, connection_url: str, username: str, password: str): + def __init__(self, connection_url: str, username: str, password: str, mrid :str): #Connect to OT self.ot_connection = GridAPPSD() @@ -52,27 +62,60 @@ def __init__(self, connection_url: str, username: str, password: str): self.broker_url = connection_url self.username = username self.password = password - self.proxy_connection = stomp.Connection([(self.broker_url.split(":")[0], int(self.broker_url.split(":")[1]))],keepalive=True) + self.proxy_connection = stomp.Connection([(self.broker_url.split(":")[0], int(self.broker_url.split(":")[1]))],keepalive=True, heartbeats=(10000,10000)) self.proxy_connection.set_listener('', FieldListener(self.ot_connection, self.proxy_connection)) self.proxy_connection.connect(self.username, self.password, wait=True) + print('Connected to Proxy') #Subscribe to messages from field self.proxy_connection.subscribe(destination=topics.BASE_FIELD_TOPIC+'.*', id=1, ack="auto") - + self.proxy_connection.subscribe(destination='goss.gridappsd.process.request.*', id=2, ack="auto") + #Subscribe to messages on OT bus self.ot_connection.subscribe(topics.field_input_topic(), self.on_message_from_ot) + + + os.environ['CIMG_CIM_PROFILE'] = 'cimhub_ufls' + os.environ['CIMG_URL'] = 'http://localhost:8889/bigdata/namespace/kb/sparql' + os.environ['CIMG_DATABASE'] = 'powergridmodel' + os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#' + os.environ['CIMG_IEC61970_301'] = '8' + os.environ['CIMG_USE_UNITS'] = 'False' + + self.database = BlazegraphConnection() + distribution_area = cim.DistributionArea(mRID=mrid) + self.network = BusBranchModel( + connection=self.database, + container=distribution_area, + distributed=False) + self.network.get_all_edges(cim.DistributionArea) + self.network.get_all_edges(cim.Substation) + + for substation in self.network.graph.get(cim.Substation,{}).values(): + print(f'Subscribing to Substation: /topic/goss.gridappsd.field.{substation.mRID}') + self.ot_connection.subscribe('/topic/goss.gridappsd.field.'+substation.mRID, self.on_message_from_ot) + + + + #self.ot_connection.subscribe(topics.BASE_FIELD_TOPIC, self.on_message_from_ot) + + def on_message_from_ot(self, headers, message): + "Receives messages coming from OT bus (GridAPPS-D) and forwards to Proxy bus" try: print(f"Received message from OT: {message}") if headers["destination"] == topics.field_input_topic(): - self.proxy_connection.send(topics.field_input_topic(), message) + self.proxy_connection.send(topics.field_input_topic(),json.dumps(message)) + + elif 'goss.gridappsd.field' in headers["destination"]: + self.proxy_connection.send(headers["destination"],json.dumps(message)) else: print(f"Unrecognized message received by OT: {message}") @@ -86,12 +129,14 @@ def on_message_from_ot(self, headers, message): parser.add_argument("username") parser.add_argument("passwd") parser.add_argument("connection_url") + parser.add_argument("mrid") opts = parser.parse_args() proxy_connection_url = opts.connection_url proxy_username = opts.username proxy_password = opts.passwd + mrid = opts.mrid - proxy_forwarder = FieldProxyForwarder(proxy_connection_url, proxy_username, proxy_password) + proxy_forwarder = FieldProxyForwarder(proxy_connection_url, proxy_username, proxy_password, mrid) while True: time.sleep(0.1) 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 724d6e0..5a0fd61 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 @@ -13,6 +13,7 @@ def __init__(self, definition: MessageBusDefinition): self._user = definition.connection_args["GRIDAPPSD_USER"] self._password = definition.connection_args["GRIDAPPSD_PASSWORD"] self._address = definition.connection_args["GRIDAPPSD_ADDRESS"] + self._use_auth_token = definition.connection_args.get("GRIDAPPSD_USE_TOKEN_AUTH", False) self.gridappsd_obj = None @@ -29,7 +30,7 @@ def connect(self): """ Connect to the concrete message bus that implements this interface. """ - self.gridappsd_obj = GridAPPSD() + self.gridappsd_obj = GridAPPSD(use_auth_token=self._use_auth_token) def subscribe(self, topic, callback): if self.gridappsd_obj is not None: diff --git a/gridappsd-field-bus-lib/gridappsd_field_bus/forwarder.py b/gridappsd-field-bus-lib/gridappsd_field_bus/forwarder.py index c744c9e..79b0c75 100644 --- a/gridappsd-field-bus-lib/gridappsd_field_bus/forwarder.py +++ b/gridappsd-field-bus-lib/gridappsd_field_bus/forwarder.py @@ -1,20 +1,47 @@ import time +from dotenv import load_dotenv import click import yaml import os +import urllib 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.') +@click.option('--username', + default=lambda: os.getenv("GRIDAPPSD_USER"), + metavar='USERNAME', + type=str, + help='Username for the connection.', + show_default="from environment variable GRIDAPPSD_USER") +@click.option('--password', + metavar='PASSWORD', + type=str, + default=lambda: os.getenv("GRIDAPPSD_PASSWORD"), + help='Password for the connection.', + show_default="from environment variable GRIDAPPSD_PASSWORD") +@click.option('--connection_url', + default=lambda: os.getenv("GRIDAPPSD_ADDRESS"), + type=str, + metavar='URL', + show_default="from environment variable GRIDAPPSD_ADDRESS", + help='Connection URL.') def start_forwarder(username, password, connection_url): """Start the field proxy forwarder with either a YAML configuration or cmd-line arguments.""" + required = [username, password, connection_url] + if not all(required): + click.echo("Username, password, and connection URL must be provided either through environment variables or command-line arguments.") + click.Abort() + + parsed = urllib.parse.urlparse(connection_url) + if not (parsed.hostname and parsed.port): + click.echo("Invalid connection URL. It must include both hostname and port.") + click.Abort() + # Use command-line arguments click.echo(f"Using command line arguments: {username}, {password}, {connection_url}") @@ -25,4 +52,5 @@ def start_forwarder(username, password, connection_url): if __name__ == '__main__': + load_dotenv() start_forwarder() \ No newline at end of file diff --git a/gridappsd-field-bus-lib/info/requirements.txt b/gridappsd-field-bus-lib/info/requirements.txt index 879d5a7..4f86d9b 100644 --- a/gridappsd-field-bus-lib/info/requirements.txt +++ b/gridappsd-field-bus-lib/info/requirements.txt @@ -1,45 +1,44 @@ asttokens==3.0.0 ; python_version >= "3.10" and python_version < "4.0" -certifi==2025.1.31 ; python_version >= "3.10" and python_version < "4.0" -charset-normalizer==3.4.1 ; python_version >= "3.10" and python_version < "4.0" +certifi==2025.7.9 ; python_version >= "3.10" and python_version < "4.0" +charset-normalizer==3.4.2 ; python_version >= "3.10" and python_version < "4.0" cim-graph==0.2.2a4 ; python_version >= "3.10" and python_version < "4.0" -click==8.1.8 ; python_version >= "3.10" and python_version < "4.0" +click==8.2.1 ; python_version >= "3.10" and python_version < "4.0" colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows") dateutils==0.6.12 ; python_version >= "3.10" and python_version < "4.0" decorator==5.2.1 ; python_version >= "3.10" and python_version < "4.0" defusedxml==0.7.1 ; python_version >= "3.10" and python_version < "4.0" docopt==0.6.2 ; python_version >= "3.10" and python_version < "4.0" -exceptiongroup==1.2.2 ; python_version == "3.10" +exceptiongroup==1.3.0 ; python_version == "3.10" executing==2.2.0 ; python_version >= "3.10" and python_version < "4.0" -gridappsd-python==2025.3.1a4 ; python_version >= "3.10" and python_version < "4.0" +gridappsd-python==2025.3.1 ; python_version >= "3.10" and python_version < "4.0" idna==3.10 ; python_version >= "3.10" and python_version < "4.0" -ipython==8.34.0 ; python_version >= "3.10" and python_version < "4.0" +ipython==8.37.0 ; python_version >= "3.10" and python_version < "4.0" isodate==0.7.2 ; python_version == "3.10" jedi==0.19.2 ; python_version >= "3.10" and python_version < "4.0" matplotlib-inline==0.1.7 ; python_version >= "3.10" and python_version < "4.0" mermaid-python==0.1 ; python_version >= "3.10" and python_version < "4.0" -mysql-connector-python==9.2.0 ; python_version >= "3.10" and python_version < "4.0" +mysql-connector-python==9.3.0 ; python_version >= "3.10" and python_version < "4.0" neo4j==5.28.1 ; python_version >= "3.10" and python_version < "4.0" nest-asyncio==1.6.0 ; python_version >= "3.10" and python_version < "4.0" oxrdflib==0.3.7 ; python_version >= "3.10" and python_version < "4.0" parso==0.8.4 ; python_version >= "3.10" and python_version < "4.0" pexpect==4.9.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform != "win32" and sys_platform != "emscripten" -prompt-toolkit==3.0.50 ; python_version >= "3.10" and python_version < "4.0" +prompt-toolkit==3.0.51 ; python_version >= "3.10" and python_version < "4.0" ptyprocess==0.7.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform != "win32" and sys_platform != "emscripten" pure-eval==0.2.3 ; python_version >= "3.10" and python_version < "4.0" -pygments==2.19.1 ; python_version >= "3.10" and python_version < "4.0" +pygments==2.19.2 ; python_version >= "3.10" and python_version < "4.0" pyoxigraph==0.3.22 ; python_version >= "3.10" and python_version < "4.0" -pyparsing==3.2.1 ; python_version >= "3.10" and python_version < "4.0" +pyparsing==3.2.3 ; python_version >= "3.10" and python_version < "4.0" python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0" -python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0" pytz==2022.7.1 ; python_version >= "3.10" and python_version < "4.0" pyyaml==6.0.2 ; python_version >= "3.10" and python_version < "4.0" -rdflib==7.1.3 ; python_version >= "3.10" and python_version < "4.0" +rdflib==7.1.4 ; python_version >= "3.10" and python_version < "4.0" requests==2.28.2 ; python_version >= "3.10" and python_version < "4.0" six==1.17.0 ; python_version >= "3.10" and python_version < "4.0" sparqlwrapper==2.0.0 ; python_version >= "3.10" and python_version < "4.0" stack-data==0.6.3 ; python_version >= "3.10" and python_version < "4.0" stomp-py==6.0.0 ; python_version >= "3.10" and python_version < "4.0" traitlets==5.14.3 ; python_version >= "3.10" and python_version < "4.0" -typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "3.12" +typing-extensions==4.14.1 ; python_version >= "3.10" and python_version < "3.12" urllib3==1.26.20 ; python_version >= "3.10" and python_version < "4.0" wcwidth==0.2.13 ; python_version >= "3.10" and python_version < "4.0" diff --git a/gridappsd-field-bus-lib/pyproject.toml b/gridappsd-field-bus-lib/pyproject.toml index 3f95a21..0b6dd67 100644 --- a/gridappsd-field-bus-lib/pyproject.toml +++ b/gridappsd-field-bus-lib/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "gridappsd-field-bus" -version = "2025.3.1" +version = "2025.6.0" description = "GridAPPS-D Field Bus Implementation" authors = [ "C. Allwardt <3979063+craig8@users.noreply.github.com>", @@ -27,7 +27,6 @@ python = ">=3.10,<4.0" gridappsd-python = ">=2025.3.1a1" cim-graph = ">=0.2.2a4" click = "^8.1.8" -"python-dotenv" = "^1.0.1" [tool.poetry.scripts] # Add things in the form diff --git a/gridappsd-python-lib/example.env b/gridappsd-python-lib/example.env new file mode 100644 index 0000000..d3cd3c7 --- /dev/null +++ b/gridappsd-python-lib/example.env @@ -0,0 +1,4 @@ +GRIDAPPSD_USER= +GRIDAPPSD_PASSWORD= +GRIDAPPSD_ADDRESS = tcp://127.0.0.1:61613 +DATABASE = powergridmodel diff --git a/gridappsd-python-lib/gridappsd/__main__.py b/gridappsd-python-lib/gridappsd/__main__.py index 9a3ed57..fe35629 100644 --- a/gridappsd-python-lib/gridappsd/__main__.py +++ b/gridappsd-python-lib/gridappsd/__main__.py @@ -44,10 +44,12 @@ from argparse import ArgumentParser from time import sleep import yaml +from dotenv import load_dotenv +from pathlib import Path from gridappsd import GridAPPSD -assert sys.version_info >= (3, 6), "Minimum version is python 3.6" +assert sys.version_info >= (3, 10), "Minimum version is python 3.10" logging.basicConfig(stream=sys.stdout, level=logging.INFO, @@ -65,7 +67,9 @@ "--run-simulation", type=argparse.FileType('r'), help="Start running a simulation from a passed simulation file.") - + group.add_argument("--env", required=False, type=str, + default=".env", + help="Load environment variables from a .env file.") opts = parser.parse_args() if opts.run_simulation: @@ -75,6 +79,15 @@ def next_timestep(simulation, timestep): sleep(1) simulation.resume() + if opts.env: + _log.info(f"Loading environment variables from {opts.env}") + env_path = Path(opts.env).expanduser() + if env_path.is_file(): + load_dotenv(opts.env) + else: + _log.error(f"Environment file {opts.env} not found. Skipping loading.") + sys.exit(1) + gappsd = GridAPPSD() run_args = yaml.safe_load(opts.run_simulation) diff --git a/gridappsd-python-lib/gridappsd/goss.py b/gridappsd-python-lib/gridappsd/goss.py index c5d3683..869f24c 100644 --- a/gridappsd-python-lib/gridappsd/goss.py +++ b/gridappsd-python-lib/gridappsd/goss.py @@ -70,6 +70,7 @@ class GRIDAPPSD_ENV_ENUM(Enum): GRIDAPPSD_ADDRESS = "GRIDAPPSD_ADDRESS" GRIDAPPSD_PORT = "GRIDAPPSD_PORT" GRIDAPPSD_PASS = "GRIDAPPSD_PASSWORD" + GRIDAPPSD_HEARTBEAT = "GRIDAPPSD_HEARTBEAT" class TimeoutError(Exception): @@ -119,6 +120,8 @@ def __init__(self, if not self.__user__ or not self.__pass__: raise ValueError("Invalid username/password specified.") + + self._heartbeat = int(os.environ.get(GRIDAPPSD_ENV_ENUM.GRIDAPPSD_HEARTBEAT.value, 10000)) self._conn = None self._ids = set() self._topic_set = set() @@ -306,7 +309,7 @@ def _make_connection(self): # send request to token topic tokenTopic = "/topic/pnnl.goss.token.topic" - tmpConn = Connection([(self.stomp_address, self.stomp_port)]) + tmpConn = Connection([(self.stomp_address, self.stomp_port)], heartbeats=(self._heartbeat, self._heartbeat)) if self._override_thread_fc is not None: tmpConn.transport.override_threading(self._override_thread_fc) tmpConn.connect(self.__user__, self.__pass__, wait=True) @@ -349,7 +352,7 @@ def on_disconnect(self, header, message): sleep(1) iter += 1 - self._conn = Connection([(self.stomp_address, self.stomp_port)]) + self._conn = Connection([(self.stomp_address, self.stomp_port)], heartbeats=(self._heartbeat, self._heartbeat)) if self._override_thread_fc is not None: self._conn.transport.override_threading(self._override_thread_fc) try: @@ -417,3 +420,14 @@ def on_error(self, header, message): _log.error("Error in callback router") _log.error(header) _log.error(message) + + def on_error(self, header, message): + _log.error("Error in callback router") + _log.error(header) + _log.error(message) + + def on_heartbeat_timeout(self): + _log.error("Heartbeat timeout") + + def on_disconnected(self): + _log.info("Disconnected") diff --git a/gridappsd-python-lib/gridappsd/simulation.py b/gridappsd-python-lib/gridappsd/simulation.py index 90c8b4d..8f21815 100644 --- a/gridappsd-python-lib/gridappsd/simulation.py +++ b/gridappsd-python-lib/gridappsd/simulation.py @@ -1,13 +1,11 @@ -from dataclasses import dataclass, field, fields -from pathlib import Path -import sys +from dataclasses import dataclass, field + import time -from copy import deepcopy -#import json import logging from typing import Dict, List, Union import gridappsd.topics as t +from gridappsd import GridAPPSD from . import json_extension as json _log = logging.getLogger(__name__) @@ -36,33 +34,33 @@ def asdict(self): @dataclass class ModelCreationConfig(ConfigBase): - load_scaling_factor: str = "1" - schedule_name: str = "ieeezipload" - z_fraction: str = "0" - i_fraction: str = "1" - p_fraction: str = "0" - randomize_zipload_fractions: bool = False - use_houses: bool = False + load_scaling_factor: str = field(default = "1") + schedule_name: str = field(default = "ieeezipload") + z_fraction: str = field(default = "0") + i_fraction: str = field(default = "1") + p_fraction: str = field(default = "0") + randomize_zipload_fractions: bool = field(default = False) + use_houses: bool = field(default = False) -__default_model_creation_config__ = ModelCreationConfig() +# __default_model_creation_config__ = ModelCreationConfig() @dataclass class SimulationArgs(ConfigBase): - start_time: str = "1655321830" - duration: str = "300" - simulator: str = "GridLAB-D" - timestep_frequency: str = "1000" - timestep_increment: str = "1000" - run_realtime: bool = True - pause_after_measurements: bool = False - simulation_name: str = "ieee13nodeckt" - power_flow_solver_method: str = "NR" - model_creation_config: ModelCreationConfig = __default_model_creation_config__ + start_time: str = field(default = "1655321830") + duration: str = field(default = "300") + simulator: str = field(default = "GridLAB-D") + timestep_frequency: str = field(default = "1000") + timestep_increment: str = field(default = "1000") + run_realtime: bool = field(default = True) + pause_after_measurements: bool = field(default = False) + simulation_name: str = field(default = "ieee13nodeckt") + power_flow_solver_method: str = field(default = "NR") + model_creation_config: ModelCreationConfig = field(default_factory = ModelCreationConfig) -__default_simulation_args__ = SimulationArgs() +# __default_simulation_args__ = SimulationArgs() @dataclass @@ -75,16 +73,16 @@ class ApplicationConfig(ConfigBase): applications: List[Application] = field(default_factory=list) -__default_application_config__ = ApplicationConfig() +# __default_application_config__ = ApplicationConfig() @dataclass class TestConfig(ConfigBase): events: List[Dict] = field(default_factory=list) - appId: str = "" + appId: str = field(default = "") -__default_test_config__ = TestConfig() +# __default_test_config__ = TestConfig() @dataclass @@ -95,18 +93,18 @@ class ServiceConfig(ConfigBase): @dataclass class PowerSystemConfig(ConfigBase): Line_name: str - GeographicalRegion_name: str = None - SubGeographicalRegion_name: str = None + GeographicalRegion_name: str = field(default = None) + SubGeographicalRegion_name: str = field(default = None) @dataclass class SimulationConfig(ConfigBase): power_system_config: PowerSystemConfig - application_config: List[ApplicationConfig] = field(default_factory=list) - simulation_config: SimulationArgs = __default_simulation_args__ + application_configs: List[ApplicationConfig] = field(default_factory=list) + simulation_config: SimulationArgs = field(default_factory=SimulationArgs) service_configs: List[ServiceConfig] = field(default_factory=list) - application_config: ApplicationConfig = __default_application_config__ - test_config: TestConfig = __default_test_config__ + application_config: ApplicationConfig = field(default_factory=ApplicationConfig) + test_config: TestConfig = field(default_factory=TestConfig) class Simulation: @@ -121,8 +119,8 @@ class Simulation: add_onmeasurement_callback, add_oncomplete_callback or add_onstart_callback method respectively. """ - def __init__(self, gapps: 'GridAPPSD', run_config: Union[Dict, SimulationConfig]): - assert type(gapps).__name__ == 'GridAPPSD', "Must be an instance of GridAPPSD" + def __init__(self, gapps: GridAPPSD, run_config: Union[Dict, SimulationConfig]): + assert isinstance(gapps, GridAPPSD), "Must be an instance of GridAPPSD" if isinstance(run_config, SimulationConfig): self._run_config = run_config.asdict() elif isinstance(run_config, dict): diff --git a/gridappsd-python-lib/pyproject.toml b/gridappsd-python-lib/pyproject.toml index c7238cb..a627dd9 100644 --- a/gridappsd-python-lib/pyproject.toml +++ b/gridappsd-python-lib/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "gridappsd-python" -version = "2025.3.1" +version = "2025.6.0" description = "A GridAPPS-D Python Adapter" authors = [ "C. Allwardt <3979063+craig8@users.noreply.github.com>", @@ -35,6 +35,8 @@ pytz = "^2022.7" dateutils = "^0.6.7" stomp-py = "6.0.0" requests = "2.28.2" +dotenv = "^0.9.9" +loguru = "^0.7.3" [tool.poetry.group.dev.dependencies] pytest = "^8.3.4" diff --git a/pyproject.toml b/pyproject.toml index b79024c..42fcc17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "gridappsd-python-workspace" -version = "2025.3.1" +version = "2025.6.0" description = "A GridAPPS-D Python Adapter" authors = [ "C. Allwardt <3979063+craig8@users.noreply.github.com>", @@ -20,6 +20,7 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.10,<4.0" + [build-system] requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api"