diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_producer_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_producer_mapper.py index a8beca2d..c278e614 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_producer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_producer_mapper.py @@ -51,8 +51,15 @@ def to_entity( temperature_out = esdl_asset.get_temperature("Out", "Supply") strategy_priority = esdl_asset.get_strategy_priority() - if esdl_asset.has_constraint(): - profile = esdl_asset.get_constraint_max_profile() + resampled_profile = None + # Check if there is an optimizer out port profile or a constraint profile. If there is both, + # only the optimizer profile is used. + if esdl_asset.has_out_optimizer_profile() or esdl_asset.has_constraint(): + if esdl_asset.has_out_optimizer_profile(): + profile = esdl_asset.get_out_port_profile() + else: + profile = esdl_asset.get_constraint_max_profile() + self.profile_interpolator = ProfileInterpolator( profile=profile, sampling_method=esdl_asset.get_sampling_method(), @@ -60,7 +67,8 @@ def to_entity( timestep=timestep, ) resampled_profile = self.profile_interpolator.get_resampled_profile() - else: + + if resampled_profile is None: resampled_profile = pd.DataFrame() contr_producer = ControllerProducer( diff --git a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py index 6e466b43..78462f3d 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -101,6 +101,19 @@ def get_profile(self) -> pd.DataFrame: ) raise ValueError(f"No profile found for asset: {self.esdl_asset.name}") + def get_out_port_profile(self) -> pd.DataFrame: + """Get the profile of the asset's out ports.""" + for port in self.esdl_asset.port: + if isinstance(port, esdl.OutPort) and port.profile.items: + for profile in port.profile: + if profile.field == "Heat_flow": + return get_data_from_profile(profile) + logger.error( + f"No profile found for asset: {self.esdl_asset.name}", + extra={"esdl_object_id": self.get_id()}, + ) + raise ValueError(f"No profile found at out port for asset: {self.esdl_asset.name}") + def get_constraint_max_profile(self) -> pd.DataFrame: """Get the profile from the asset's maximum constraint.""" for constraint in self.esdl_asset.constraint: @@ -211,6 +224,20 @@ def has_profile(self) -> bool: return True return False + def has_out_optimizer_profile(self) -> bool: + """Checks if an asset has an optimizer profile assigned to its out port.""" + for port in self.esdl_asset.port: + if isinstance(port, esdl.OutPort) and port.profile.items: + # There is a profile on the out port + for profile in port.profile: + if ( + hasattr(profile, "dataSource") + and hasattr(profile.dataSource, "name") + and profile.dataSource.name == "Optimizer" + ): + return True + return False + def has_constraint(self) -> bool: """Checks if an asset has a constraint assigned to it.""" if self.esdl_asset.constraint.items: diff --git a/src/omotes_simulator_core/entities/utility/influxdb_reader.py b/src/omotes_simulator_core/entities/utility/influxdb_reader.py index 615ab1df..c225c738 100644 --- a/src/omotes_simulator_core/entities/utility/influxdb_reader.py +++ b/src/omotes_simulator_core/entities/utility/influxdb_reader.py @@ -15,16 +15,40 @@ """Module to read the esdl profiles from an energy system.""" import logging +from datetime import datetime +from typing import cast import esdl import pandas as pd from esdl.esdl_handler import EnergySystemHandler -from esdl.profiles.influxdbprofilemanager import InfluxDBProfileManager +from esdl.profiles.influxdbprofilemanager import ConnectionSettings, InfluxDBProfileManager from esdl.units.conversion import ENERGY_IN_J, POWER_IN_W, convert_to_unit logger = logging.getLogger(__name__) +def _normalize_influx_filters(filters: str | None) -> list[dict[str, str]]: + """Parse and normalize filters to the format expected by load_influxdb. + + The upstream parser may return dictionaries with a `key` field while + load_influxdb expects `tag`. This shim keeps compatibility across versions. + """ + parsed_filters = InfluxDBProfileManager._parse_esdl_profile_filters(filters) + normalized_filters: list[dict[str, str]] = [] + for influx_filter in parsed_filters: + tag = influx_filter.get("tag") or influx_filter.get("key") + value = influx_filter.get("value") + if not tag or value is None: + continue + normalized_filters.append( + { + "tag": str(tag), + "value": str(value).strip().strip("\"").strip("'"), + } + ) + return normalized_filters + + def parse_esdl_profiles(esh: EnergySystemHandler) -> dict[str, pd.DataFrame]: """Method to parse the esdl profiles from an energy system. @@ -48,7 +72,11 @@ def get_data_from_profile(esdl_profile: esdl.InfluxDBProfile) -> pd.DataFrame: :return: pandas.DataFrame with the data """ influx_cred_map: dict[str, tuple[str, str]] = {} - profile_host = esdl_profile.host + profile_host = str(esdl_profile.host) + profile_port = int(esdl_profile.port) + profile_database = str(esdl_profile.database) + profile_measurement = str(esdl_profile.measurement) + profile_field = str(esdl_profile.field) ssl_setting = False if "https" in profile_host: profile_host = profile_host[8:] @@ -56,25 +84,36 @@ def get_data_from_profile(esdl_profile: esdl.InfluxDBProfile) -> pd.DataFrame: elif "http" in profile_host: profile_host = profile_host[7:] # why is this here? - if esdl_profile.port == 443: + if profile_port == 443: ssl_setting = True - influx_host = f"{profile_host}:{esdl_profile.port}" + influx_host = f"{profile_host}:{profile_port}" if influx_host in influx_cred_map: (username, password) = influx_cred_map[influx_host] else: username = None password = None - time_series_data = InfluxDBProfileManager.create_esdl_influxdb_profile_manager( - esdl_profile, - username, - password, - ssl_setting, - ssl_setting, + conn_settings = ConnectionSettings( + host=profile_host, + port=profile_port, + database=profile_database, + username=username or "", + password=password or "", + ssl=ssl_setting, + verify_ssl=ssl_setting, + ) + time_series_data = InfluxDBProfileManager(conn_settings) + time_series_data.load_influxdb( + measurement=profile_measurement, + fields=[profile_field], + from_datetime=cast(datetime, esdl_profile.startDate), + to_datetime=cast(datetime, esdl_profile.endDate), + filters=_normalize_influx_filters(str(esdl_profile.filters) + if esdl_profile.filters else None), ) # Error check start and end dates of profiles - # I do not thing this is required since you set it in mapeditor. + # I do not think this is required since you set it in mapeditor. if time_series_data.end_datetime != esdl_profile.endDate: logger.error( f"The user input profile end datetime: {esdl_profile.endDate} does not match the end" diff --git a/unit_test/entities/test_esdl_object.py b/unit_test/entities/test_esdl_object.py index 6bbf7a86..1b57cbcd 100644 --- a/unit_test/entities/test_esdl_object.py +++ b/unit_test/entities/test_esdl_object.py @@ -16,6 +16,7 @@ """Test esdl object class.""" import unittest from pathlib import Path +from unittest.mock import MagicMock, Mock import esdl from pandas.testing import assert_frame_equal @@ -423,6 +424,40 @@ def test_get_asset_by_id(self): # Assert self.assertEqual(asset.esdl_asset, producer.esdl_asset) + def test_has_out_optimizer_profile(self): + """Test to see if the method to check for optimizer profiles works correctly.""" + # Arrange + asset_with_mocked_profiles = self.esdl_object.get_all_assets_of_type("producer")[0] + + non_optimizer_profile = Mock() + non_optimizer_profile.dataSource = Mock() + non_optimizer_profile.dataSource.name = "NotOptimizer" + + optimizer_profile = Mock() + optimizer_profile.dataSource = Mock() + optimizer_profile.dataSource.name = "Optimizer" + + mocked_profiles = MagicMock() + mocked_profiles.items = [non_optimizer_profile, optimizer_profile] + mocked_profiles.__iter__.return_value = iter([non_optimizer_profile, optimizer_profile]) + + mocked_out_port = Mock(spec=esdl.OutPort) + mocked_out_port.profile = mocked_profiles + + mocked_esdl_asset = Mock() + mocked_esdl_asset.port = [mocked_out_port] + asset_with_mocked_profiles.esdl_asset = mocked_esdl_asset + + asset_without_profile = self.esdl_object.get_all_assets_of_type("pipe")[0] + + # Act + has_optimizer_profile = asset_with_mocked_profiles.has_out_optimizer_profile() + has_no_optimizer_profile = asset_without_profile.has_out_optimizer_profile() + + # Assert + self.assertTrue(has_optimizer_profile) + self.assertIs(has_no_optimizer_profile, False) + class StringEsdlAssetMapperTest(unittest.TestCase): """Class to test conversion from esdl asset to string and back."""