From 6070734ca9bb37e16b8aba2d6ea83278cec885e3 Mon Sep 17 00:00:00 2001 From: clydeu Date: Tue, 3 Jun 2025 17:32:28 +1000 Subject: [PATCH 01/13] createOpendssModel Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 189 +++++++++++++++++++++- src/zepben/eas/client/opendss.py | 23 +++ test/test_eas_client.py | 243 ++++++++++++++++++++++++++++ 3 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 src/zepben/eas/client/opendss.py diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 36da373..4deb450 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -15,6 +15,7 @@ from urllib3.exceptions import InsecureRequestWarning from zepben.auth import AuthMethod, ZepbenTokenFetcher, create_token_fetcher, create_token_fetcher_managed_identity +from zepben.eas.client.opendss import OpenDssConfig from zepben.eas.client.study import Study from zepben.eas.client.util import construct_url from zepben.eas.client.work_package import WorkPackageConfig, FixedTime, TimePeriod, ForecastConfig, FeederConfigs @@ -797,7 +798,7 @@ def run_hosting_capacity_calibration(self, calibration_name: str, local_calibrat :return: The HTTP response received from the Evolve App Server after attempting to run the calibration """ return get_event_loop().run_until_complete( - self.async_run_hosting_capacity_calibration(calibration_name, local_calibration_time)) + self.async_run_opendss_export(calibration_name, local_calibration_time)) async def async_run_hosting_capacity_calibration(self, calibration_name: str, calibration_time_local: Optional[str] = None): @@ -924,3 +925,189 @@ async def async_get_hosting_capacity_calibration_sets(self): else: response = await response.text() return response + + def run_opendss_export(self, config: OpenDssConfig): + """ + Send request to run an opendss export + :param config: The OpenDssConfig for running the export + :return: The HTTP response received from the Evolve App Server after attempting to run the opendss export + """ + return get_event_loop().run_until_complete(self.async_run_opendss_export(config)) + + async def async_run_opendss_export(self, config: OpenDssConfig): + """ + Send asynchronous request to run an opendss export + :param config: The OpenDssConfig for running the export + :return: The HTTP response received from the Evolve App Server after attempting to run the opendss export + """ + with warnings.catch_warnings(): + if not self._verify_certificate: + warnings.filterwarnings("ignore", category=InsecureRequestWarning) + json = { + "query": """ + mutation createOpenDssModel($input: OpenDssModelInput!) { + createOpenDssModel(input: $input) + } + """, + "variables": { + "input": { + "modelName": config.model_name, + "isPublic": config.is_public, + "generationSpec": { + "modelOptions": { + "feeder": config.feeder, + "scenario": config.scenario, + "year": config.year + }, + "modulesConfiguration": { + "common": { + **({"fixedTime": config.load_time.time.isoformat()} + if isinstance(config.load_time, FixedTime) else {}), + **({"timePeriod": { + "start": config.load_time.start_time.isoformat(), + "end": config.load_time.end_time.isoformat(), + }} if isinstance(config.load_time, TimePeriod) else {}) + }, + **({"generator": { + **({"model": { + "vmPu": config.generator_config.model.vm_pu, + "vMinPu": config.generator_config.model.vmin_pu, + "vMaxPu": config.generator_config.model.vmax_pu, + "loadModel": config.generator_config.model.load_model, + "collapseSWER": config.generator_config.model.collapse_swer, + "calibration": config.generator_config.model.calibration, + "pFactorBaseExports": config.generator_config.model.p_factor_base_exports, + "pFactorForecastPv": config.generator_config.model.p_factor_forecast_pv, + "pFactorBaseImports": config.generator_config.model.p_factor_base_imports, + "fixSinglePhaseLoads": config.generator_config.model.fix_single_phase_loads, + "maxSinglePhaseLoad": config.generator_config.model.max_single_phase_load, + "fixOverloadingConsumers": config.generator_config.model.fix_overloading_consumers, + "maxLoadTxRatio": config.generator_config.model.max_load_tx_ratio, + "maxGenTxRatio": config.generator_config.model.max_gen_tx_ratio, + "fixUndersizedServiceLines": config.generator_config.model.fix_undersized_service_lines, + "maxLoadServiceLineRatio": config.generator_config.model.max_load_service_line_ratio, + "maxLoadLvLineRatio": config.generator_config.model.max_load_lv_line_ratio, + "collapseLvNetworks": config.generator_config.model.collapse_lv_networks, + "feederScenarioAllocationStrategy": config.generator_config.model.feeder_scenario_allocation_strategy and config.generator_config.model.feeder_scenario_allocation_strategy.name, + "closedLoopVRegEnabled": config.generator_config.model.closed_loop_v_reg_enabled, + "closedLoopVRegReplaceAll": config.generator_config.model.closed_loop_v_reg_replace_all, + "closedLoopVRegSetPoint": config.generator_config.model.closed_loop_v_reg_set_point, + "closedLoopVBand": config.generator_config.model.closed_loop_v_band, + "closedLoopTimeDelay": config.generator_config.model.closed_loop_time_delay, + "closedLoopVLimit": config.generator_config.model.closed_loop_v_limit, + "defaultTapChangerTimeDelay": config.generator_config.model.default_tap_changer_time_delay, + "defaultTapChangerSetPointPu": config.generator_config.model.default_tap_changer_set_point_pu, + "defaultTapChangerBand": config.generator_config.model.default_tap_changer_band, + "splitPhaseDefaultLoadLossPercentage": config.generator_config.model.split_phase_default_load_loss_percentage, + "splitPhaseLVKV": config.generator_config.model.split_phase_lv_kv, + "swerVoltageToLineVoltage": config.generator_config.model.swer_voltage_to_line_voltage, + "loadPlacement": config.generator_config.model.load_placement and config.generator_config.model.load_placement.name, + "loadIntervalLengthHours": config.generator_config.model.load_interval_length_hours, + "meterPlacementConfig": config.generator_config.model.meter_placement_config and { + "feederHead": config.generator_config.model.meter_placement_config.feeder_head, + "distTransformers": config.generator_config.model.meter_placement_config.dist_transformers, + "switchMeterPlacementConfigs": config.generator_config.model.meter_placement_config.switch_meter_placement_configs and [ + { + "meterSwitchClass": spc.meter_switch_class and spc.meter_switch_class.name, + "namePattern": spc.name_pattern + } for spc in + config.generator_config.model.meter_placement_config.switch_meter_placement_configs + ], + "energyConsumerMeterGroup": config.generator_config.model.meter_placement_config.energy_consumer_meter_group + }, + "seed": config.generator_config.model.seed, + "defaultLoadWatts": config.generator_config.model.default_load_watts, + "defaultGenWatts": config.generator_config.model.default_gen_watts, + "defaultLoadVar": config.generator_config.model.default_load_var, + "defaultGenVar": config.generator_config.model.default_gen_var, + "transformerTapSettings": config.generator_config.model.transformer_tap_settings + }} if config.generator_config.model else {}), + **({"solve": { + "normVMinPu": config.generator_config.solve.norm_vmin_pu, + "normVMaxPu": config.generator_config.solve.norm_vmax_pu, + "emergVMinPu": config.generator_config.solve.emerg_vmin_pu, + "emergVMaxPu": config.generator_config.solve.emerg_vmax_pu, + "baseFrequency": config.generator_config.solve.base_frequency, + "voltageBases": config.generator_config.solve.voltage_bases, + "maxIter": config.generator_config.solve.max_iter, + "maxControlIter": config.generator_config.solve.max_control_iter, + "mode": config.generator_config.solve.mode and config.generator_config.solve.mode.name, + "stepSizeMinutes": config.generator_config.solve.step_size_minutes + }} if config.generator_config.solve else {}), + **({"rawResults": { + "energyMeterVoltagesRaw": config.generator_config.raw_results.energy_meter_voltages_raw, + "energyMetersRaw": config.generator_config.raw_results.energy_meters_raw, + "resultsPerMeter": config.generator_config.raw_results.results_per_meter, + "overloadsRaw": config.generator_config.raw_results.overloads_raw, + "voltageExceptionsRaw": config.generator_config.raw_results.voltage_exceptions_raw + }} if config.generator_config.raw_results else {}) + }} if config.generator_config else {}) + } + } + } + } + } + if self._verify_certificate: + sslcontext = ssl.create_default_context(cafile=self._ca_filename) + + async with self.session.post( + construct_url(protocol=self._protocol, host=self._host, port=self._port, path="/api/graphql"), + headers=self._get_request_headers(), + json=json, + ssl=sslcontext if self._verify_certificate else False + ) as response: + if response.ok: + response = await response.json() + else: + response = await response.text() + return response + + def get_hosting_capacity_calibration_run(self, id: str): + """ + Retrieve information of a hosting capacity calibration run + :param id: The calibration run ID + :return: The HTTP response received from the Evolve App Server after requesting calibration run info + """ + return get_event_loop().run_until_complete(self.async_get_hosting_capacity_calibration_run(id)) + + async def async_get_hosting_capacity_calibration_run(self, id: str): + """ + Retrieve information of a hosting capacity calibration run + :param id: The calibration run ID + :return: The HTTP response received from the Evolve App Server after requesting calibration run info + """ + with warnings.catch_warnings(): + if not self._verify_certificate: + warnings.filterwarnings("ignore", category=InsecureRequestWarning) + json = { + "query": """ + query getCalibrationRun($id: ID!) { + getCalibrationRun(calibrationRunId: $id) { + id + name + workflowId + runId + startAt + completedAt + status + } + } + """, + "variables": { + "id": id + } + } + if self._verify_certificate: + sslcontext = ssl.create_default_context(cafile=self._ca_filename) + + async with self.session.post( + construct_url(protocol=self._protocol, host=self._host, port=self._port, path="/api/graphql"), + headers=self._get_request_headers(), + json=json, + ssl=sslcontext if self._verify_certificate else False + ) as response: + if response.ok: + response = await response.json() + else: + response = await response.text() + return response \ No newline at end of file diff --git a/src/zepben/eas/client/opendss.py b/src/zepben/eas/client/opendss.py new file mode 100644 index 0000000..1a54d2e --- /dev/null +++ b/src/zepben/eas/client/opendss.py @@ -0,0 +1,23 @@ +# Copyright 2020 Zeppelin Bend Pty Ltd +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import dataclass +from typing import Union, Optional +from zepben.eas.client.work_package import GeneratorConfig, TimePeriod, FixedTime + +__all__ = [ + "OpenDssConfig" +] + +@dataclass +class OpenDssConfig: + """ A data class representing the configuration for a opendss export """ + scenario: str + year: int + feeder: str + load_time: Union[TimePeriod, FixedTime] + generator_config: Optional[GeneratorConfig] = None + model_name: Optional[str] = None + is_public: Optional[bool] = None \ No newline at end of file diff --git a/test/test_eas_client.py b/test/test_eas_client.py index b534db0..31b30c0 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -17,9 +17,14 @@ from zepben.auth import ZepbenTokenFetcher from zepben.eas import EasClient, Study, FeederConfig, ForecastConfig, FixedTimeLoadOverride +from zepben.eas import EasClient, Study, SolveConfig +from zepben.eas.client.opendss import OpenDssConfig from zepben.eas.client.study import Result from zepben.eas.client.work_package import WorkPackageConfig, TimePeriod, FeederConfigs, TimePeriodLoadOverride, \ FixedTime +from zepben.eas.client.work_package import WorkPackageConfig, TimePeriod, GeneratorConfig, ModelConfig, \ + FeederScenarioAllocationStrategy, LoadPlacement, MeterPlacementConfig, SwitchMeterPlacementConfig, SwitchClass, \ + SolveMode, RawResultsConfig mock_host = ''.join(random.choices(string.ascii_lowercase, k=10)) mock_port = random.randrange(1024) @@ -753,3 +758,241 @@ def test_get_hosting_capacity_calibration_sets_no_verify_success(httpserver: HTT res = eas_client.get_hosting_capacity_calibration_sets() httpserver.check_assertions() assert res == ["one", "two", "three"] + +def run_opendss_export_request_handler(request): + actual_body = json.loads(request.data.decode()) + query = " ".join(actual_body['query'].split()) + + assert query == "mutation createOpenDssModel($input: OpenDssModelInput!) { createOpenDssModel(input: $input) }" + assert actual_body['variables'] == { + "input": { + "modelName": "TEST OPENDSS MODEL 1", + "isPublic": True, + "generationSpec": { + "modelOptions": { + "feeder": "feeder1", + "scenario": "scenario1", + "year": 2024 + }, + "modulesConfiguration": { + "common": { + "timePeriod": { + "start": "2022-04-01T00:00:00", + "end": "2023-04-01T00:00:00", + } + }, + "generator": { + "model": { + "vmPu": 1.0, + "vMinPu": 0.80, + "vMaxPu": 1.15, + "loadModel": 1, + "collapseSWER": False, + "calibration": False, + "pFactorBaseExports": 0.95, + "pFactorBaseImports": 0.90, + "pFactorForecastPv": 1.0, + "fixSinglePhaseLoads": True, + "maxSinglePhaseLoad": 30000.0, + "fixOverloadingConsumers": True, + "maxLoadTxRatio": 3.0, + "maxGenTxRatio": 10.0, + "fixUndersizedServiceLines": True, + "maxLoadServiceLineRatio": 1.5, + "maxLoadLvLineRatio": 2.0, + "collapseLvNetworks": False, + "feederScenarioAllocationStrategy": "ADDITIVE", + "closedLoopVRegEnabled": True, + "closedLoopVRegReplaceAll": True, + "closedLoopVRegSetPoint": 0.985, + "closedLoopVBand": 2.0, + "closedLoopTimeDelay": 100, + "closedLoopVLimit": 1.1, + "defaultTapChangerTimeDelay": 100, + "defaultTapChangerSetPointPu": 1.0, + "defaultTapChangerBand": 2.0, + "splitPhaseDefaultLoadLossPercentage": 0.4, + "splitPhaseLVKV": 0.25, + "swerVoltageToLineVoltage": [ + [230, 400], + [240, 415], + [250, 433], + [6350, 11000], + [6400, 11000], + [12700, 22000], + [19100, 33000] + ], + "loadPlacement": "PER_USAGE_POINT", + "loadIntervalLengthHours": 0.5, + "meterPlacementConfig": { + "feederHead": True, + "distTransformers": True, + "switchMeterPlacementConfigs": [ + { + "meterSwitchClass": "LOAD_BREAK_SWITCH", + "namePattern": ".*" + } + ], + "energyConsumerMeterGroup": "meter group 1" + }, + "seed": 42, + "defaultLoadWatts": [100.0, 200.0, 300.0], + "defaultGenWatts": [50.0, 150.0, 250.0], + "defaultLoadVar": [10.0, 20.0, 30.0], + "defaultGenVar": [5.0, 15.0, 25.0], + "transformerTapSettings": "tap-3" + }, + "solve": { + "normVMinPu": 0.9, + "normVMaxPu": 1.054, + "emergVMinPu": 0.8, + "emergVMaxPu": 1.1, + "baseFrequency": 50, + "voltageBases": [0.4, 0.433, 6.6, 11.0, 22.0, 33.0, 66.0, 132.0], + "maxIter": 25, + "maxControlIter": 20, + "mode": "YEARLY", + "stepSizeMinutes": 60 + }, + "rawResults": { + "energyMeterVoltagesRaw": True, + "energyMetersRaw": True, + "resultsPerMeter": True, + "overloadsRaw": True, + "voltageExceptionsRaw": True + } + } + } + } + } + } + + return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") + +OPENDSS_CONFIG = OpenDssConfig( + scenario="scenario1", + year=2024, + feeder="feeder1", + load_time=TimePeriod( + datetime(2022, 4, 1), + datetime(2023, 4, 1) + ), + model_name="TEST OPENDSS MODEL 1", + generator_config=GeneratorConfig( + ModelConfig( + vm_pu=1.0, + vmin_pu=0.80, + vmax_pu=1.15, + load_model=1, + collapse_swer=False, + calibration=False, + p_factor_base_exports=0.95, + p_factor_base_imports=0.90, + p_factor_forecast_pv=1.0, + fix_single_phase_loads=True, + max_single_phase_load=30000.0, + fix_overloading_consumers=True, + max_load_tx_ratio=3.0, + max_gen_tx_ratio=10.0, + fix_undersized_service_lines=True, + max_load_service_line_ratio=1.5, + max_load_lv_line_ratio=2.0, + collapse_lv_networks=False, + feeder_scenario_allocation_strategy=FeederScenarioAllocationStrategy.ADDITIVE, + closed_loop_v_reg_enabled=True, + closed_loop_v_reg_replace_all=True, + closed_loop_v_reg_set_point=0.985, + closed_loop_v_band=2.0, + closed_loop_time_delay=100, + closed_loop_v_limit=1.1, + default_tap_changer_time_delay=100, + default_tap_changer_set_point_pu=1.0, + default_tap_changer_band=2.0, + split_phase_default_load_loss_percentage=0.4, + split_phase_lv_kv=0.25, + swer_voltage_to_line_voltage= [ + [230, 400], + [240, 415], + [250, 433], + [6350, 11000], + [6400, 11000], + [12700, 22000], + [19100, 33000] + ], + load_placement=LoadPlacement.PER_USAGE_POINT, + load_interval_length_hours=0.5, + meter_placement_config=MeterPlacementConfig( + True, + True, + [SwitchMeterPlacementConfig(SwitchClass.LOAD_BREAK_SWITCH, ".*")], + "meter group 1"), + seed=42, + default_load_watts=[100.0, 200.0, 300.0], + default_gen_watts=[50.0, 150.0, 250.0], + default_load_var=[10.0, 20.0, 30.0], + default_gen_var=[5.0, 15.0, 25.0], + transformer_tap_settings="tap-3" + ), + SolveConfig( + norm_vmin_pu=0.9, + norm_vmax_pu=1.054, + emerg_vmin_pu=0.8, + emerg_vmax_pu=1.1, + base_frequency=50, + voltage_bases=[0.4, 0.433, 6.6, 11.0, 22.0, 33.0, 66.0, 132.0], + max_iter=25, + max_control_iter=20, + mode=SolveMode.YEARLY, + step_size_minutes=60 + ), + RawResultsConfig( + energy_meter_voltages_raw=True, + energy_meters_raw=True, + results_per_meter=True, + overloads_raw=True, + voltage_exceptions_raw=True + ) + ), + is_public=True) + +def test_run_opendss_export_no_verify_success(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler(run_opendss_export_request_handler) + res = eas_client.run_opendss_export(OPENDSS_CONFIG) + httpserver.check_assertions() + assert res == {"result": "success"} + + +def test_run_opendss_export_invalid_certificate_failure(ca: trustme.CA, httpserver: HTTPServer): + with trustme.Blob(b"invalid ca").tempfile() as ca_filename: + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=True, + ca_filename=ca_filename + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_json({"result": "success"}) + with pytest.raises(ssl.SSLError): + eas_client.run_opendss_export(OPENDSS_CONFIG) + + +def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver: HTTPServer): + with ca.cert_pem.tempfile() as ca_filename: + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=True, + ca_filename=ca_filename + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + run_opendss_export_request_handler) + res = eas_client.run_opendss_export(OPENDSS_CONFIG) + httpserver.check_assertions() + assert res == {"result": "success"} \ No newline at end of file From 20170acc7bb1295fd048f90c7424854d04b11535 Mon Sep 17 00:00:00 2001 From: clydeu Date: Tue, 3 Jun 2025 21:55:52 +1000 Subject: [PATCH 02/13] pagedOpenDssModels Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 149 ++++++++++++-- src/zepben/eas/client/opendss.py | 36 +++- test/test_eas_client.py | 289 +++++++++++++++++++++++++++- 3 files changed, 452 insertions(+), 22 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 4deb450..5cecce0 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -15,7 +15,7 @@ from urllib3.exceptions import InsecureRequestWarning from zepben.auth import AuthMethod, ZepbenTokenFetcher, create_token_fetcher, create_token_fetcher_managed_identity -from zepben.eas.client.opendss import OpenDssConfig +from zepben.eas.client.opendss import OpenDssConfig, GetOpenDssModelsFilterInput, GetOpenDssModelsSortCriteriaInput from zepben.eas.client.study import Study from zepben.eas.client.util import construct_url from zepben.eas.client.work_package import WorkPackageConfig, FixedTime, TimePeriod, ForecastConfig, FeederConfigs @@ -798,7 +798,7 @@ def run_hosting_capacity_calibration(self, calibration_name: str, local_calibrat :return: The HTTP response received from the Evolve App Server after attempting to run the calibration """ return get_event_loop().run_until_complete( - self.async_run_opendss_export(calibration_name, local_calibration_time)) + self.async_run_hosting_capacity_calibration(calibration_name, local_calibration_time)) async def async_run_hosting_capacity_calibration(self, calibration_name: str, calibration_time_local: Optional[str] = None): @@ -962,7 +962,7 @@ async def async_run_opendss_export(self, config: OpenDssConfig): "modulesConfiguration": { "common": { **({"fixedTime": config.load_time.time.isoformat()} - if isinstance(config.load_time, FixedTime) else {}), + if isinstance(config.load_time, FixedTime) else {}), **({"timePeriod": { "start": config.load_time.start_time.isoformat(), "end": config.load_time.end_time.isoformat(), @@ -1062,18 +1062,28 @@ async def async_run_opendss_export(self, config: OpenDssConfig): response = await response.text() return response - def get_hosting_capacity_calibration_run(self, id: str): + def get_paged_opendss_models(self, limit: Optional[int] = None, offset: Optional[int] = None, + query_filter: Optional[GetOpenDssModelsFilterInput] = None, + query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): """ - Retrieve information of a hosting capacity calibration run - :param id: The calibration run ID + Retrieve a paginated opendss export run information + :param limit: The number of opendss export runs to retrieve + :param offset: The number of opendss export runs to skip + :param query_filter: The filter to apply to the query + :param query_sort: The sorting to apply to the query :return: The HTTP response received from the Evolve App Server after requesting calibration run info """ - return get_event_loop().run_until_complete(self.async_get_hosting_capacity_calibration_run(id)) + return get_event_loop().run_until_complete(self.async_get_paged_opendss_models(limit, offset, query_filter, query_sort)) - async def async_get_hosting_capacity_calibration_run(self, id: str): + async def async_get_paged_opendss_models(self, limit: Optional[int] = None, offset: Optional[int] = None, + query_filter: Optional[GetOpenDssModelsFilterInput] = None, + query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): """ Retrieve information of a hosting capacity calibration run - :param id: The calibration run ID + :param limit: The number of opendss export runs to retrieve + :param offset: The number of opendss export runs to skip + :param query_filter: The filter to apply to the query + :param query_sort: The sorting to apply to the query :return: The HTTP response received from the Evolve App Server after requesting calibration run info """ with warnings.catch_warnings(): @@ -1081,20 +1091,123 @@ async def async_get_hosting_capacity_calibration_run(self, id: str): warnings.filterwarnings("ignore", category=InsecureRequestWarning) json = { "query": """ - query getCalibrationRun($id: ID!) { - getCalibrationRun(calibrationRunId: $id) { + query pagedOpenDssModels($limit: Int, $offset: Long, $filter: GetOpenDssModelsFilterInput, $sort: GetOpenDssModelsSortCriteriaInput) { + pagedOpenDssModels(limit: $limit, offset: $offset, filter: $filter,sort: $sort) { + totalCount + offset, + models { id name - workflowId - runId - startAt - completedAt - status + createdAt + createdBy + state + downloadUrl + isPublic + errors + generationSpec { + modelOptions{ + scenario + year + feeder + } + modulesConfiguration { + common { + timePeriod { + start + end + } + } + generator { + model { + vmPu + vMinPu + vMaxPu + loadModel + collapseSWER + calibration + pFactorBaseExports + pFactorForecastPv + pFactorBaseImports + fixSinglePhaseLoads + maxSinglePhaseLoad + fixOverloadingConsumers + maxLoadTxRatio + maxGenTxRatio + fixUndersizedServiceLines + maxLoadServiceLineRatio + maxLoadLvLineRatio + collapseLvNetworks + feederScenarioAllocationStrategy + closedLoopVRegEnabled + closedLoopVRegReplaceAll + closedLoopVRegSetPoint + closedLoopVBand + closedLoopTimeDelay + closedLoopVLimit + defaultTapChangerTimeDelay + defaultTapChangerSetPointPu + defaultTapChangerBand + splitPhaseDefaultLoadLossPercentage + splitPhaseLVKV + swerVoltageToLineVoltage + loadPlacement + loadIntervalLengthHours + meterPlacementConfig { + feederHead + distTransformers + switchMeterPlacementConfigs { + meterSwitchClass + namePattern + } + energyConsumerMeterGroup + } + seed + defaultLoadWatts + defaultGenWatts + defaultLoadVar + defaultGenVar + transformerTapSettings + } + solve { + normVMinPu + normVMaxPu + emergVMinPu + emergVMaxPu + baseFrequency + voltageBases + maxIter + maxControlIter + mode + stepSizeMinutes + } + rawResults { + energyMeterVoltagesRaw + energyMetersRaw + resultsPerMeter + overloadsRaw + voltageExceptionsRaw + } + } + } + } } } + } """, "variables": { - "id": id + **({"limit": limit} if limit is not None else {}), + **({"offset": offset} if offset is not None else {}), + **({"filter": { + "name": query_filter.name, + "isPublic": query_filter.is_public, + "state": query_filter.state and [state.name for state in query_filter.state] + }} if query_filter else {}), + **({"sort": { + "name": query_sort.name and query_sort.name.name, + "createdAt": query_sort.created_at and query_sort.created_at.name, + "state": query_sort.state and query_sort.state.name, + "isPublic": query_sort.is_public and query_sort.is_public.name + }} if query_sort else {}) } } if self._verify_certificate: @@ -1110,4 +1223,4 @@ async def async_get_hosting_capacity_calibration_run(self, id: str): response = await response.json() else: response = await response.text() - return response \ No newline at end of file + return response diff --git a/src/zepben/eas/client/opendss.py b/src/zepben/eas/client/opendss.py index 1a54d2e..d7ca7df 100644 --- a/src/zepben/eas/client/opendss.py +++ b/src/zepben/eas/client/opendss.py @@ -4,11 +4,16 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import dataclass -from typing import Union, Optional +from enum import Enum +from typing import Union, Optional, List from zepben.eas.client.work_package import GeneratorConfig, TimePeriod, FixedTime __all__ = [ - "OpenDssConfig" + "OpenDssConfig", + "OpenDssModelState", + "GetOpenDssModelsFilterInput", + "Order", + "GetOpenDssModelsSortCriteriaInput" ] @dataclass @@ -20,4 +25,29 @@ class OpenDssConfig: load_time: Union[TimePeriod, FixedTime] generator_config: Optional[GeneratorConfig] = None model_name: Optional[str] = None - is_public: Optional[bool] = None \ No newline at end of file + is_public: Optional[bool] = None + +class OpenDssModelState(Enum): + COULD_NOT_START = "COULD_NOT_START" + CREATION = "CREATION" + COMPLETED = "COMPLETED" + FAILED = "FAILED" + +@dataclass +class GetOpenDssModelsFilterInput: + """ A data class representing the filter to apply to the opendss export run paginated query """ + name: Optional[str] = None + is_public: Optional[int] = None + state: Optional[List[OpenDssModelState]] = None + +class Order(Enum): + ASC = "ASC" + DESC = "DESC" + +@dataclass +class GetOpenDssModelsSortCriteriaInput: + """ A data class representing the sort criteria to apply to the opendss export run paginated query """ + name: Optional[Order] = None + created_at: Optional[Order] = None + state: Optional[Order] = None + is_public: Optional[Order] = None diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 31b30c0..c58ad78 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -18,7 +18,8 @@ from zepben.eas import EasClient, Study, FeederConfig, ForecastConfig, FixedTimeLoadOverride from zepben.eas import EasClient, Study, SolveConfig -from zepben.eas.client.opendss import OpenDssConfig +from zepben.eas.client.opendss import OpenDssConfig, GetOpenDssModelsFilterInput, OpenDssModelState, GetOpenDssModelsSortCriteriaInput, \ + Order from zepben.eas.client.study import Result from zepben.eas.client.work_package import WorkPackageConfig, TimePeriod, FeederConfigs, TimePeriodLoadOverride, \ FixedTime @@ -995,4 +996,290 @@ def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver run_opendss_export_request_handler) res = eas_client.run_opendss_export(OPENDSS_CONFIG) httpserver.check_assertions() + assert res == {"result": "success"} + +def get_paged_opendss_models_request_handler(request): + actual_body = json.loads(request.data.decode()) + query = " ".join(actual_body['query'].split()) + + expected_query = """ + query pagedOpenDssModels($limit: Int, $offset: Long, $filter: GetOpenDssModelsFilterInput, $sort: GetOpenDssModelsSortCriteriaInput) { + pagedOpenDssModels(limit: $limit, offset: $offset, filter: $filter,sort: $sort) { + totalCount + offset, + models { + id + name + createdAt + createdBy + state + downloadUrl + isPublic + errors + generationSpec { + modelOptions{ + scenario + year + feeder + } + modulesConfiguration { + common { + timePeriod { + start + end + } + } + generator { + model { + vmPu + vMinPu + vMaxPu + loadModel + collapseSWER + calibration + pFactorBaseExports + pFactorForecastPv + pFactorBaseImports + fixSinglePhaseLoads + maxSinglePhaseLoad + fixOverloadingConsumers + maxLoadTxRatio + maxGenTxRatio + fixUndersizedServiceLines + maxLoadServiceLineRatio + maxLoadLvLineRatio + collapseLvNetworks + feederScenarioAllocationStrategy + closedLoopVRegEnabled + closedLoopVRegReplaceAll + closedLoopVRegSetPoint + closedLoopVBand + closedLoopTimeDelay + closedLoopVLimit + defaultTapChangerTimeDelay + defaultTapChangerSetPointPu + defaultTapChangerBand + splitPhaseDefaultLoadLossPercentage + splitPhaseLVKV + swerVoltageToLineVoltage + loadPlacement + loadIntervalLengthHours + meterPlacementConfig { + feederHead + distTransformers + switchMeterPlacementConfigs { + meterSwitchClass + namePattern + } + energyConsumerMeterGroup + } + seed + defaultLoadWatts + defaultGenWatts + defaultLoadVar + defaultGenVar + transformerTapSettings + } + solve { + normVMinPu + normVMaxPu + emergVMinPu + emergVMaxPu + baseFrequency + voltageBases + maxIter + maxControlIter + mode + stepSizeMinutes + } + rawResults { + energyMeterVoltagesRaw + energyMetersRaw + resultsPerMeter + overloadsRaw + voltageExceptionsRaw + } + } + } + } + } + } + } + """ + assert query == " ".join(line.strip() for line in expected_query.strip().splitlines()) + assert actual_body['variables'] == { + "limit": 5, + "offset": 0, + "filter": { + "name": "TEST OPENDSS MODEL 1", + "isPublic": True, + "state": ["COMPLETED"], + }, + "sort": { + "state": "ASC", + "createdAt": None, + "name": None, + "isPublic": None + } + } + + return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") + + +def test_get_paged_opendss_models_no_verify_success(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + get_paged_opendss_models_request_handler) + res = eas_client.get_paged_opendss_models( + 5, 0, GetOpenDssModelsFilterInput("TEST OPENDSS MODEL 1", True, [OpenDssModelState.COMPLETED]), + GetOpenDssModelsSortCriteriaInput(state=Order.ASC)) + httpserver.check_assertions() + assert res == {"result": "success"} + + +def test_get_paged_opendss_models_invalid_certificate_failure(ca: trustme.CA, httpserver: HTTPServer): + with trustme.Blob(b"invalid ca").tempfile() as ca_filename: + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=True, + ca_filename=ca_filename + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_json({"result": "success"}) + with pytest.raises(ssl.SSLError): + eas_client.get_paged_opendss_models() + + +def get_paged_opendss_models_no_param_request_handler(request): + actual_body = json.loads(request.data.decode()) + query = " ".join(actual_body['query'].split()) + + expected_query = """ + query pagedOpenDssModels($limit: Int, $offset: Long, $filter: GetOpenDssModelsFilterInput, $sort: GetOpenDssModelsSortCriteriaInput) { + pagedOpenDssModels(limit: $limit, offset: $offset, filter: $filter,sort: $sort) { + totalCount + offset, + models { + id + name + createdAt + createdBy + state + downloadUrl + isPublic + errors + generationSpec { + modelOptions{ + scenario + year + feeder + } + modulesConfiguration { + common { + timePeriod { + start + end + } + } + generator { + model { + vmPu + vMinPu + vMaxPu + loadModel + collapseSWER + calibration + pFactorBaseExports + pFactorForecastPv + pFactorBaseImports + fixSinglePhaseLoads + maxSinglePhaseLoad + fixOverloadingConsumers + maxLoadTxRatio + maxGenTxRatio + fixUndersizedServiceLines + maxLoadServiceLineRatio + maxLoadLvLineRatio + collapseLvNetworks + feederScenarioAllocationStrategy + closedLoopVRegEnabled + closedLoopVRegReplaceAll + closedLoopVRegSetPoint + closedLoopVBand + closedLoopTimeDelay + closedLoopVLimit + defaultTapChangerTimeDelay + defaultTapChangerSetPointPu + defaultTapChangerBand + splitPhaseDefaultLoadLossPercentage + splitPhaseLVKV + swerVoltageToLineVoltage + loadPlacement + loadIntervalLengthHours + meterPlacementConfig { + feederHead + distTransformers + switchMeterPlacementConfigs { + meterSwitchClass + namePattern + } + energyConsumerMeterGroup + } + seed + defaultLoadWatts + defaultGenWatts + defaultLoadVar + defaultGenVar + transformerTapSettings + } + solve { + normVMinPu + normVMaxPu + emergVMinPu + emergVMaxPu + baseFrequency + voltageBases + maxIter + maxControlIter + mode + stepSizeMinutes + } + rawResults { + energyMeterVoltagesRaw + energyMetersRaw + resultsPerMeter + overloadsRaw + voltageExceptionsRaw + } + } + } + } + } + } + } + """ + assert query == " ".join(line.strip() for line in expected_query.strip().splitlines()) + assert actual_body['variables'] == { } + + return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") + +def test_get_paged_opendss_models_valid_certificate_success(ca: trustme.CA, httpserver: HTTPServer): + with ca.cert_pem.tempfile() as ca_filename: + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=True, + ca_filename=ca_filename + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + get_paged_opendss_models_no_param_request_handler) + res = eas_client.get_paged_opendss_models() + httpserver.check_assertions() assert res == {"result": "success"} \ No newline at end of file From 808d5e44d776376f30e09d88d5a90eaada82c210 Mon Sep 17 00:00:00 2001 From: clydeu Date: Tue, 3 Jun 2025 22:31:53 +1000 Subject: [PATCH 03/13] download url Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 34 +++++++++++++++++++ test/test_eas_client.py | 52 ++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 5cecce0..ac85c6e 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -7,6 +7,7 @@ import warnings from asyncio import get_event_loop from hashlib import sha256 +from http import HTTPStatus from json import dumps from typing import Optional @@ -1224,3 +1225,36 @@ async def async_get_paged_opendss_models(self, limit: Optional[int] = None, offs else: response = await response.text() return response + + def get_opendss_model_download_url(self, id: int): + """ + Retrieve a download url for the specified opendss export run id + :param id: The opendss export run ID + :return: The HTTP response received from the Evolve App Server after requesting calibration run info + """ + return get_event_loop().run_until_complete(self.async_get_opendss_model_download_url(id)) + + async def async_get_opendss_model_download_url(self, id: int): + """ + Retrieve a download url for the specified opendss export run id + :param id: The opendss export run ID + :return: The HTTP response received from the Evolve App Server after requesting calibration run info + """ + with warnings.catch_warnings(): + if not self._verify_certificate: + warnings.filterwarnings("ignore", category=InsecureRequestWarning) + + if self._verify_certificate: + sslcontext = ssl.create_default_context(cafile=self._ca_filename) + + async with self.session.post( + construct_url(protocol=self._protocol, host=self._host, port=self._port, path=f"/api/opendss-model/{id}"), + headers=self._get_request_headers(), + ssl=sslcontext if self._verify_certificate else False, + allow_redirects=False + ) as response: + if HTTPStatus(response.status).is_redirection: + response = response.headers["Location"] + else: + response = await response.text() + return response diff --git a/test/test_eas_client.py b/test/test_eas_client.py index c58ad78..c561610 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -8,6 +8,7 @@ import ssl import string from datetime import datetime +from http import HTTPStatus from unittest import mock import pytest @@ -1282,4 +1283,53 @@ def test_get_paged_opendss_models_valid_certificate_success(ca: trustme.CA, http get_paged_opendss_models_no_param_request_handler) res = eas_client.get_paged_opendss_models() httpserver.check_assertions() - assert res == {"result": "success"} \ No newline at end of file + assert res == {"result": "success"} + +def test_get_opendss_model_download_url_no_verify_success(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + httpserver.expect_oneshot_request("/api/opendss-model/1").respond_with_response(Response( + status=HTTPStatus.FOUND, + headers={"Location": "https://example.com/download/1"} + )) + res = eas_client.get_opendss_model_download_url(1) + httpserver.check_assertions() + assert res == "https://example.com/download/1" + +def test_get_opendss_model_download_url_invalid_certificate_failure(ca: trustme.CA, httpserver: HTTPServer): + with trustme.Blob(b"invalid ca").tempfile() as ca_filename: + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=True, + ca_filename=ca_filename + ) + + httpserver.expect_oneshot_request("/api/opendss-model/1").respond_with_response(Response( + status=HTTPStatus.FOUND, + headers={"Location": "https://example.com/download/1"} + )) + with pytest.raises(ssl.SSLError): + eas_client.get_opendss_model_download_url(1) + + +def test_get_opendss_model_download_url_valid_certificate_success(ca: trustme.CA, httpserver: HTTPServer): + with ca.cert_pem.tempfile() as ca_filename: + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=True, + ca_filename=ca_filename + ) + + httpserver.expect_oneshot_request("/api/opendss-model/1").respond_with_response(Response( + status=HTTPStatus.FOUND, + headers={"Location": "https://example.com/download/1"} + )) + res = eas_client.get_opendss_model_download_url(1) + httpserver.check_assertions() + assert res == "https://example.com/download/1" \ No newline at end of file From 698309eac4ba52606954e31335bcae27560b15b0 Mon Sep 17 00:00:00 2001 From: clydeu Date: Wed, 4 Jun 2025 13:59:33 +1000 Subject: [PATCH 04/13] use get method for download url route. Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 2 +- test/test_eas_client.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index ac85c6e..84c52a6 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -1247,7 +1247,7 @@ async def async_get_opendss_model_download_url(self, id: int): if self._verify_certificate: sslcontext = ssl.create_default_context(cafile=self._ca_filename) - async with self.session.post( + async with self.session.get( construct_url(protocol=self._protocol, host=self._host, port=self._port, path=f"/api/opendss-model/{id}"), headers=self._get_request_headers(), ssl=sslcontext if self._verify_certificate else False, diff --git a/test/test_eas_client.py b/test/test_eas_client.py index c561610..7163fe1 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -1292,7 +1292,7 @@ def test_get_opendss_model_download_url_no_verify_success(httpserver: HTTPServer verify_certificate=False ) - httpserver.expect_oneshot_request("/api/opendss-model/1").respond_with_response(Response( + httpserver.expect_oneshot_request("/api/opendss-model/1", method="GET").respond_with_response(Response( status=HTTPStatus.FOUND, headers={"Location": "https://example.com/download/1"} )) @@ -1309,7 +1309,7 @@ def test_get_opendss_model_download_url_invalid_certificate_failure(ca: trustme. ca_filename=ca_filename ) - httpserver.expect_oneshot_request("/api/opendss-model/1").respond_with_response(Response( + httpserver.expect_oneshot_request("/api/opendss-model/1", method="GET").respond_with_response(Response( status=HTTPStatus.FOUND, headers={"Location": "https://example.com/download/1"} )) @@ -1326,7 +1326,7 @@ def test_get_opendss_model_download_url_valid_certificate_success(ca: trustme.CA ca_filename=ca_filename ) - httpserver.expect_oneshot_request("/api/opendss-model/1").respond_with_response(Response( + httpserver.expect_oneshot_request("/api/opendss-model/1", method="GET").respond_with_response(Response( status=HTTPStatus.FOUND, headers={"Location": "https://example.com/download/1"} )) From 111a69cd3c37c96ec434f6ef5472e5b0fc30ce2c Mon Sep 17 00:00:00 2001 From: clydeu Date: Wed, 4 Jun 2025 14:04:54 +1000 Subject: [PATCH 05/13] changelog. Signed-off-by: clydeu --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 83a735a..36a8df1 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ * Added basic client method to run a hosting capacity calibration and method to query its status. * Added basic client method to run a hosting capacity work package cost estimation. * Added `FixedTimeLoadOverride` and `TimePeriodLoadOverride` class +* Added basic client method to run a opendss export, query its information and get a download url for the exported model. ### Enhancements * Added work package config documentation. From f0238102bc8d761d45620c69c24f9e710fe2c4be Mon Sep 17 00:00:00 2001 From: clydeu Date: Wed, 4 Jun 2025 14:14:43 +1000 Subject: [PATCH 06/13] fix wordings. Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 10 +++++----- src/zepben/eas/client/opendss.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 84c52a6..cc1c673 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -1072,7 +1072,7 @@ def get_paged_opendss_models(self, limit: Optional[int] = None, offset: Optional :param offset: The number of opendss export runs to skip :param query_filter: The filter to apply to the query :param query_sort: The sorting to apply to the query - :return: The HTTP response received from the Evolve App Server after requesting calibration run info + :return: The HTTP response received from the Evolve App Server after requesting opendss export run information """ return get_event_loop().run_until_complete(self.async_get_paged_opendss_models(limit, offset, query_filter, query_sort)) @@ -1080,12 +1080,12 @@ async def async_get_paged_opendss_models(self, limit: Optional[int] = None, offs query_filter: Optional[GetOpenDssModelsFilterInput] = None, query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): """ - Retrieve information of a hosting capacity calibration run + Retrieve a paginated opendss export run information :param limit: The number of opendss export runs to retrieve :param offset: The number of opendss export runs to skip :param query_filter: The filter to apply to the query :param query_sort: The sorting to apply to the query - :return: The HTTP response received from the Evolve App Server after requesting calibration run info + :return: The HTTP response received from the Evolve App Server after requesting opendss export run information """ with warnings.catch_warnings(): if not self._verify_certificate: @@ -1230,7 +1230,7 @@ def get_opendss_model_download_url(self, id: int): """ Retrieve a download url for the specified opendss export run id :param id: The opendss export run ID - :return: The HTTP response received from the Evolve App Server after requesting calibration run info + :return: The HTTP response received from the Evolve App Server after requesting opendss export model download url """ return get_event_loop().run_until_complete(self.async_get_opendss_model_download_url(id)) @@ -1238,7 +1238,7 @@ async def async_get_opendss_model_download_url(self, id: int): """ Retrieve a download url for the specified opendss export run id :param id: The opendss export run ID - :return: The HTTP response received from the Evolve App Server after requesting calibration run info + :return: The HTTP response received from the Evolve App Server after requesting opendss export model download url """ with warnings.catch_warnings(): if not self._verify_certificate: diff --git a/src/zepben/eas/client/opendss.py b/src/zepben/eas/client/opendss.py index d7ca7df..3116e4e 100644 --- a/src/zepben/eas/client/opendss.py +++ b/src/zepben/eas/client/opendss.py @@ -18,7 +18,7 @@ @dataclass class OpenDssConfig: - """ A data class representing the configuration for a opendss export """ + """ A data class representing the configuration for an opendss export """ scenario: str year: int feeder: str From 4537985e1acca20657b0b168e3a90348a8e1866e Mon Sep 17 00:00:00 2001 From: clydeu Date: Wed, 4 Jun 2025 14:51:14 +1000 Subject: [PATCH 07/13] change http status check to enum code for compatibility. Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index cc1c673..1aa02a9 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -1253,7 +1253,7 @@ async def async_get_opendss_model_download_url(self, id: int): ssl=sslcontext if self._verify_certificate else False, allow_redirects=False ) as response: - if HTTPStatus(response.status).is_redirection: + if response.status == HTTPStatus.FOUND: response = response.headers["Location"] else: response = await response.text() From bf0fb369bec763d1e969be7bc4d894cfc25f99b4 Mon Sep 17 00:00:00 2001 From: clydeu Date: Wed, 4 Jun 2025 21:02:11 +1000 Subject: [PATCH 08/13] add timezone to OpenDssConfig. Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 1 + src/zepben/eas/client/opendss.py | 2 ++ test/test_eas_client.py | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 1aa02a9..0a53a06 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -962,6 +962,7 @@ async def async_run_opendss_export(self, config: OpenDssConfig): }, "modulesConfiguration": { "common": { + "timeZone": config.time_zone.__str__(), **({"fixedTime": config.load_time.time.isoformat()} if isinstance(config.load_time, FixedTime) else {}), **({"timePeriod": { diff --git a/src/zepben/eas/client/opendss.py b/src/zepben/eas/client/opendss.py index 3116e4e..a27774d 100644 --- a/src/zepben/eas/client/opendss.py +++ b/src/zepben/eas/client/opendss.py @@ -4,6 +4,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import dataclass +from datetime import tzinfo from enum import Enum from typing import Union, Optional, List from zepben.eas.client.work_package import GeneratorConfig, TimePeriod, FixedTime @@ -22,6 +23,7 @@ class OpenDssConfig: scenario: str year: int feeder: str + time_zone: tzinfo load_time: Union[TimePeriod, FixedTime] generator_config: Optional[GeneratorConfig] = None model_name: Optional[str] = None diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 7163fe1..9d0a15e 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -7,7 +7,7 @@ import random import ssl import string -from datetime import datetime +from datetime import datetime, timezone, timedelta from http import HTTPStatus from unittest import mock @@ -778,6 +778,7 @@ def run_opendss_export_request_handler(request): }, "modulesConfiguration": { "common": { + "timeZone": "UTC+10:00", "timePeriod": { "start": "2022-04-01T00:00:00", "end": "2023-04-01T00:00:00", @@ -875,6 +876,7 @@ def run_opendss_export_request_handler(request): scenario="scenario1", year=2024, feeder="feeder1", + time_zone=timezone(timedelta(hours=10)), load_time=TimePeriod( datetime(2022, 4, 1), datetime(2023, 4, 1) From ce2b415787b61beba549ac0fdf86d2b262e98473 Mon Sep 17 00:00:00 2001 From: clydeu Date: Wed, 18 Jun 2025 14:01:21 +1000 Subject: [PATCH 09/13] addressed Ant's comments. Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 8 +- test/test_eas_client.py | 121 +++------------------------- 2 files changed, 15 insertions(+), 114 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 0a53a06..04b02c9 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -1064,7 +1064,9 @@ async def async_run_opendss_export(self, config: OpenDssConfig): response = await response.text() return response - def get_paged_opendss_models(self, limit: Optional[int] = None, offset: Optional[int] = None, + def get_paged_opendss_models(self, + limit: Optional[int] = None, + offset: Optional[int] = None, query_filter: Optional[GetOpenDssModelsFilterInput] = None, query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): """ @@ -1077,7 +1079,9 @@ def get_paged_opendss_models(self, limit: Optional[int] = None, offset: Optional """ return get_event_loop().run_until_complete(self.async_get_paged_opendss_models(limit, offset, query_filter, query_sort)) - async def async_get_paged_opendss_models(self, limit: Optional[int] = None, offset: Optional[int] = None, + async def async_get_paged_opendss_models(self, + limit: Optional[int] = None, + offset: Optional[int] = None, query_filter: Optional[GetOpenDssModelsFilterInput] = None, query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): """ diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 9d0a15e..69eaac8 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -1001,11 +1001,7 @@ def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver httpserver.check_assertions() assert res == {"result": "success"} -def get_paged_opendss_models_request_handler(request): - actual_body = json.loads(request.data.decode()) - query = " ".join(actual_body['query'].split()) - - expected_query = """ +get_paged_opendss_models_query = """ query pagedOpenDssModels($limit: Int, $offset: Long, $filter: GetOpenDssModelsFilterInput, $sort: GetOpenDssModelsSortCriteriaInput) { pagedOpenDssModels(limit: $limit, offset: $offset, filter: $filter,sort: $sort) { totalCount @@ -1109,7 +1105,12 @@ def get_paged_opendss_models_request_handler(request): } } """ - assert query == " ".join(line.strip() for line in expected_query.strip().splitlines()) + +def get_paged_opendss_models_request_handler(request): + actual_body = json.loads(request.data.decode()) + query = " ".join(actual_body['query'].split()) + + assert query == " ".join(line.strip() for line in get_paged_opendss_models_query.strip().splitlines()) assert actual_body['variables'] == { "limit": 5, "offset": 0, @@ -1163,111 +1164,7 @@ def get_paged_opendss_models_no_param_request_handler(request): actual_body = json.loads(request.data.decode()) query = " ".join(actual_body['query'].split()) - expected_query = """ - query pagedOpenDssModels($limit: Int, $offset: Long, $filter: GetOpenDssModelsFilterInput, $sort: GetOpenDssModelsSortCriteriaInput) { - pagedOpenDssModels(limit: $limit, offset: $offset, filter: $filter,sort: $sort) { - totalCount - offset, - models { - id - name - createdAt - createdBy - state - downloadUrl - isPublic - errors - generationSpec { - modelOptions{ - scenario - year - feeder - } - modulesConfiguration { - common { - timePeriod { - start - end - } - } - generator { - model { - vmPu - vMinPu - vMaxPu - loadModel - collapseSWER - calibration - pFactorBaseExports - pFactorForecastPv - pFactorBaseImports - fixSinglePhaseLoads - maxSinglePhaseLoad - fixOverloadingConsumers - maxLoadTxRatio - maxGenTxRatio - fixUndersizedServiceLines - maxLoadServiceLineRatio - maxLoadLvLineRatio - collapseLvNetworks - feederScenarioAllocationStrategy - closedLoopVRegEnabled - closedLoopVRegReplaceAll - closedLoopVRegSetPoint - closedLoopVBand - closedLoopTimeDelay - closedLoopVLimit - defaultTapChangerTimeDelay - defaultTapChangerSetPointPu - defaultTapChangerBand - splitPhaseDefaultLoadLossPercentage - splitPhaseLVKV - swerVoltageToLineVoltage - loadPlacement - loadIntervalLengthHours - meterPlacementConfig { - feederHead - distTransformers - switchMeterPlacementConfigs { - meterSwitchClass - namePattern - } - energyConsumerMeterGroup - } - seed - defaultLoadWatts - defaultGenWatts - defaultLoadVar - defaultGenVar - transformerTapSettings - } - solve { - normVMinPu - normVMaxPu - emergVMinPu - emergVMaxPu - baseFrequency - voltageBases - maxIter - maxControlIter - mode - stepSizeMinutes - } - rawResults { - energyMeterVoltagesRaw - energyMetersRaw - resultsPerMeter - overloadsRaw - voltageExceptionsRaw - } - } - } - } - } - } - } - """ - assert query == " ".join(line.strip() for line in expected_query.strip().splitlines()) + assert query == " ".join(line.strip() for line in get_paged_opendss_models_query.strip().splitlines()) assert actual_body['variables'] == { } return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") @@ -1334,4 +1231,4 @@ def test_get_opendss_model_download_url_valid_certificate_success(ca: trustme.CA )) res = eas_client.get_opendss_model_download_url(1) httpserver.check_assertions() - assert res == "https://example.com/download/1" \ No newline at end of file + assert res == "https://example.com/download/1" From 4f37080afb37ae3635f23b8b9f1c778488af4a90 Mon Sep 17 00:00:00 2001 From: clydeu Date: Thu, 19 Jun 2025 15:22:47 +1000 Subject: [PATCH 10/13] addressed Max's comments. Signed-off-by: clydeu --- src/zepben/eas/client/eas_client.py | 34 +++++++++++++++-------------- test/test_eas_client.py | 8 +++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 04b02c9..acd28be 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -1064,11 +1064,12 @@ async def async_run_opendss_export(self, config: OpenDssConfig): response = await response.text() return response - def get_paged_opendss_models(self, - limit: Optional[int] = None, - offset: Optional[int] = None, - query_filter: Optional[GetOpenDssModelsFilterInput] = None, - query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): + def get_paged_opendss_models( + self, + limit: Optional[int] = None, + offset: Optional[int] = None, + query_filter: Optional[GetOpenDssModelsFilterInput] = None, + query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): """ Retrieve a paginated opendss export run information :param limit: The number of opendss export runs to retrieve @@ -1079,11 +1080,12 @@ def get_paged_opendss_models(self, """ return get_event_loop().run_until_complete(self.async_get_paged_opendss_models(limit, offset, query_filter, query_sort)) - async def async_get_paged_opendss_models(self, - limit: Optional[int] = None, - offset: Optional[int] = None, - query_filter: Optional[GetOpenDssModelsFilterInput] = None, - query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): + async def async_get_paged_opendss_models( + self, + limit: Optional[int] = None, + offset: Optional[int] = None, + query_filter: Optional[GetOpenDssModelsFilterInput] = None, + query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): """ Retrieve a paginated opendss export run information :param limit: The number of opendss export runs to retrieve @@ -1231,18 +1233,18 @@ async def async_get_paged_opendss_models(self, response = await response.text() return response - def get_opendss_model_download_url(self, id: int): + def get_opendss_model_download_url(self, run_id: int): """ Retrieve a download url for the specified opendss export run id - :param id: The opendss export run ID + :param run_id: The opendss export run ID :return: The HTTP response received from the Evolve App Server after requesting opendss export model download url """ - return get_event_loop().run_until_complete(self.async_get_opendss_model_download_url(id)) + return get_event_loop().run_until_complete(self.async_get_opendss_model_download_url(run_id)) - async def async_get_opendss_model_download_url(self, id: int): + async def async_get_opendss_model_download_url(self, run_id: int): """ Retrieve a download url for the specified opendss export run id - :param id: The opendss export run ID + :param run_id: The opendss export run ID :return: The HTTP response received from the Evolve App Server after requesting opendss export model download url """ with warnings.catch_warnings(): @@ -1253,7 +1255,7 @@ async def async_get_opendss_model_download_url(self, id: int): sslcontext = ssl.create_default_context(cafile=self._ca_filename) async with self.session.get( - construct_url(protocol=self._protocol, host=self._host, port=self._port, path=f"/api/opendss-model/{id}"), + construct_url(protocol=self._protocol, host=self._host, port=self._port, path=f"/api/opendss-model/{run_id}"), headers=self._get_request_headers(), ssl=sslcontext if self._verify_certificate else False, allow_redirects=False diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 69eaac8..dd0b5f9 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -761,6 +761,7 @@ def test_get_hosting_capacity_calibration_sets_no_verify_success(httpserver: HTT httpserver.check_assertions() assert res == ["one", "two", "three"] + def run_opendss_export_request_handler(request): actual_body = json.loads(request.data.decode()) query = " ".join(actual_body['query'].split()) @@ -872,6 +873,7 @@ def run_opendss_export_request_handler(request): return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") + OPENDSS_CONFIG = OpenDssConfig( scenario="scenario1", year=2024, @@ -959,6 +961,7 @@ def run_opendss_export_request_handler(request): ), is_public=True) + def test_run_opendss_export_no_verify_success(httpserver: HTTPServer): eas_client = EasClient( LOCALHOST, @@ -1001,6 +1004,7 @@ def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver httpserver.check_assertions() assert res == {"result": "success"} + get_paged_opendss_models_query = """ query pagedOpenDssModels($limit: Int, $offset: Long, $filter: GetOpenDssModelsFilterInput, $sort: GetOpenDssModelsSortCriteriaInput) { pagedOpenDssModels(limit: $limit, offset: $offset, filter: $filter,sort: $sort) { @@ -1106,6 +1110,7 @@ def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver } """ + def get_paged_opendss_models_request_handler(request): actual_body = json.loads(request.data.decode()) query = " ".join(actual_body['query'].split()) @@ -1169,6 +1174,7 @@ def get_paged_opendss_models_no_param_request_handler(request): return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") + def test_get_paged_opendss_models_valid_certificate_success(ca: trustme.CA, httpserver: HTTPServer): with ca.cert_pem.tempfile() as ca_filename: eas_client = EasClient( @@ -1184,6 +1190,7 @@ def test_get_paged_opendss_models_valid_certificate_success(ca: trustme.CA, http httpserver.check_assertions() assert res == {"result": "success"} + def test_get_opendss_model_download_url_no_verify_success(httpserver: HTTPServer): eas_client = EasClient( LOCALHOST, @@ -1199,6 +1206,7 @@ def test_get_opendss_model_download_url_no_verify_success(httpserver: HTTPServer httpserver.check_assertions() assert res == "https://example.com/download/1" + def test_get_opendss_model_download_url_invalid_certificate_failure(ca: trustme.CA, httpserver: HTTPServer): with trustme.Blob(b"invalid ca").tempfile() as ca_filename: eas_client = EasClient( From 3eb86cc1d827af3280ab8419bc79f775604b2f02 Mon Sep 17 00:00:00 2001 From: clydeu Date: Thu, 19 Jun 2025 15:41:46 +1000 Subject: [PATCH 11/13] addressed Max's comments. Signed-off-by: clydeu --- src/zepben/eas/client/opendss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zepben/eas/client/opendss.py b/src/zepben/eas/client/opendss.py index a27774d..4d1b9d6 100644 --- a/src/zepben/eas/client/opendss.py +++ b/src/zepben/eas/client/opendss.py @@ -1,4 +1,4 @@ -# Copyright 2020 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this From ee068fda80d55e97be1c1e65769bcc72c4aa5885 Mon Sep 17 00:00:00 2001 From: clydeu Date: Thu, 19 Jun 2025 15:43:24 +1000 Subject: [PATCH 12/13] addressed Max's comments. Signed-off-by: clydeu --- .idea/copyright/MPLv2.xml | 2 +- src/zepben/eas/client/opendss.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.idea/copyright/MPLv2.xml b/.idea/copyright/MPLv2.xml index 34ce855..10616a8 100644 --- a/.idea/copyright/MPLv2.xml +++ b/.idea/copyright/MPLv2.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/src/zepben/eas/client/opendss.py b/src/zepben/eas/client/opendss.py index 4d1b9d6..73dd489 100644 --- a/src/zepben/eas/client/opendss.py +++ b/src/zepben/eas/client/opendss.py @@ -17,6 +17,7 @@ "GetOpenDssModelsSortCriteriaInput" ] + @dataclass class OpenDssConfig: """ A data class representing the configuration for an opendss export """ @@ -29,12 +30,14 @@ class OpenDssConfig: model_name: Optional[str] = None is_public: Optional[bool] = None + class OpenDssModelState(Enum): COULD_NOT_START = "COULD_NOT_START" CREATION = "CREATION" COMPLETED = "COMPLETED" FAILED = "FAILED" + @dataclass class GetOpenDssModelsFilterInput: """ A data class representing the filter to apply to the opendss export run paginated query """ @@ -42,10 +45,12 @@ class GetOpenDssModelsFilterInput: is_public: Optional[int] = None state: Optional[List[OpenDssModelState]] = None + class Order(Enum): ASC = "ASC" DESC = "DESC" + @dataclass class GetOpenDssModelsSortCriteriaInput: """ A data class representing the sort criteria to apply to the opendss export run paginated query """ From 2b14e658d784ba86c2dd1beed493c301c3a79b85 Mon Sep 17 00:00:00 2001 From: Roberto Marquez Date: Tue, 24 Jun 2025 12:08:39 +1000 Subject: [PATCH 13/13] Fixes after rebase. Signed-off-by: Roberto Marquez --- src/zepben/eas/client/eas_client.py | 12 +- src/zepben/eas/client/hc_commons.py | 13 +- src/zepben/eas/client/opendss.py | 1 + src/zepben/eas/client/work_package.py | 2 - test/test_eas_client.py | 197 +++++++++++++------------- 5 files changed, 114 insertions(+), 111 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index acd28be..7fb3389 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -1078,7 +1078,8 @@ def get_paged_opendss_models( :param query_sort: The sorting to apply to the query :return: The HTTP response received from the Evolve App Server after requesting opendss export run information """ - return get_event_loop().run_until_complete(self.async_get_paged_opendss_models(limit, offset, query_filter, query_sort)) + return get_event_loop().run_until_complete( + self.async_get_paged_opendss_models(limit, offset, query_filter, query_sort)) async def async_get_paged_opendss_models( self, @@ -1255,10 +1256,11 @@ async def async_get_opendss_model_download_url(self, run_id: int): sslcontext = ssl.create_default_context(cafile=self._ca_filename) async with self.session.get( - construct_url(protocol=self._protocol, host=self._host, port=self._port, path=f"/api/opendss-model/{run_id}"), - headers=self._get_request_headers(), - ssl=sslcontext if self._verify_certificate else False, - allow_redirects=False + construct_url(protocol=self._protocol, host=self._host, port=self._port, + path=f"/api/opendss-model/{run_id}"), + headers=self._get_request_headers(), + ssl=sslcontext if self._verify_certificate else False, + allow_redirects=False ) as response: if response.status == HTTPStatus.FOUND: response = response.headers["Location"] diff --git a/src/zepben/eas/client/hc_commons.py b/src/zepben/eas/client/hc_commons.py index 5055177..a94a7f7 100644 --- a/src/zepben/eas/client/hc_commons.py +++ b/src/zepben/eas/client/hc_commons.py @@ -17,19 +17,20 @@ "BASIC_RESULTS_CONFIG", ] -from zepben.eas import StoredResultsConfig, RawResultsConfig, MetricsResultsConfig, ResultsConfig +from build.lib.zepben.eas.client.work_package import ResultsConfig +from zepben.eas import StoredResultsConfig, RawResultsConfig, MetricsResultsConfig STORED_RESULTS_CONFIG_STORE_NONE = StoredResultsConfig( energy_meters_raw=False, energy_meter_voltages_raw=False, - over_loads_raw=False, + overloads_raw=False, voltage_exceptions_raw=False ) STORED_RESULTS_CONFIG_STORE_ALL = StoredResultsConfig( energy_meters_raw=True, energy_meter_voltages_raw=True, - over_loads_raw=True, + overloads_raw=True, voltage_exceptions_raw=True ) @@ -37,7 +38,7 @@ energy_meters_raw=True, energy_meter_voltages_raw=True, results_per_meter=True, - over_loads_raw=True, + overloads_raw=True, voltage_exceptions_raw=True ) @@ -45,7 +46,7 @@ energy_meters_raw=True, energy_meter_voltages_raw=True, results_per_meter=True, - over_loads_raw=True, + overloads_raw=True, voltage_exceptions_raw=True ) @@ -69,7 +70,7 @@ STANDARD_RESULTS_CONFIG = ResultsConfig( raw_config=RAW_RESULTS_CONFIG_STANDARD, metrics_config=METRICS_RESULTS_CONFIG_CALCULATE_PERFORMANCE_METRICS, - stored_results_config=StoredResultsConfig(voltage_exceptions_raw=True, over_loads_raw=True) + stored_results_config=StoredResultsConfig(voltage_exceptions_raw=True, overloads_raw=True) ) BASIC_RESULTS_CONFIG = ResultsConfig( diff --git a/src/zepben/eas/client/opendss.py b/src/zepben/eas/client/opendss.py index 73dd489..ae21075 100644 --- a/src/zepben/eas/client/opendss.py +++ b/src/zepben/eas/client/opendss.py @@ -7,6 +7,7 @@ from datetime import tzinfo from enum import Enum from typing import Union, Optional, List + from zepben.eas.client.work_package import GeneratorConfig, TimePeriod, FixedTime __all__ = [ diff --git a/src/zepben/eas/client/work_package.py b/src/zepben/eas/client/work_package.py index 9d26048..d8f9d81 100644 --- a/src/zepben/eas/client/work_package.py +++ b/src/zepben/eas/client/work_package.py @@ -69,7 +69,6 @@ class SwitchMeterPlacementConfig: @dataclass class FixedTimeLoadOverride: - load_watts: Optional[float] """ The reading to be used to override load watts @@ -95,7 +94,6 @@ class FixedTimeLoadOverride: @dataclass class TimePeriodLoadOverride: - load_watts: Optional[List[float]] """ A list of readings to be used to override load watts. diff --git a/test/test_eas_client.py b/test/test_eas_client.py index dd0b5f9..035b713 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -17,12 +17,13 @@ from werkzeug import Response from zepben.auth import ZepbenTokenFetcher -from zepben.eas import EasClient, Study, FeederConfig, ForecastConfig, FixedTimeLoadOverride from zepben.eas import EasClient, Study, SolveConfig -from zepben.eas.client.opendss import OpenDssConfig, GetOpenDssModelsFilterInput, OpenDssModelState, GetOpenDssModelsSortCriteriaInput, \ +from zepben.eas import FeederConfig, ForecastConfig, FixedTimeLoadOverride +from zepben.eas.client.opendss import OpenDssConfig, GetOpenDssModelsFilterInput, OpenDssModelState, \ + GetOpenDssModelsSortCriteriaInput, \ Order from zepben.eas.client.study import Result -from zepben.eas.client.work_package import WorkPackageConfig, TimePeriod, FeederConfigs, TimePeriodLoadOverride, \ +from zepben.eas.client.work_package import FeederConfigs, TimePeriodLoadOverride, \ FixedTime from zepben.eas.client.work_package import WorkPackageConfig, TimePeriod, GeneratorConfig, ModelConfig, \ FeederScenarioAllocationStrategy, LoadPlacement, MeterPlacementConfig, SwitchMeterPlacementConfig, SwitchClass, \ @@ -829,15 +830,15 @@ def run_opendss_export_request_handler(request): "loadPlacement": "PER_USAGE_POINT", "loadIntervalLengthHours": 0.5, "meterPlacementConfig": { - "feederHead": True, - "distTransformers": True, - "switchMeterPlacementConfigs": [ - { - "meterSwitchClass": "LOAD_BREAK_SWITCH", - "namePattern": ".*" - } - ], - "energyConsumerMeterGroup": "meter group 1" + "feederHead": True, + "distTransformers": True, + "switchMeterPlacementConfigs": [ + { + "meterSwitchClass": "LOAD_BREAK_SWITCH", + "namePattern": ".*" + } + ], + "energyConsumerMeterGroup": "meter group 1" }, "seed": 42, "defaultLoadWatts": [100.0, 200.0, 300.0], @@ -875,91 +876,91 @@ def run_opendss_export_request_handler(request): OPENDSS_CONFIG = OpenDssConfig( - scenario="scenario1", - year=2024, - feeder="feeder1", - time_zone=timezone(timedelta(hours=10)), - load_time=TimePeriod( - datetime(2022, 4, 1), - datetime(2023, 4, 1) - ), - model_name="TEST OPENDSS MODEL 1", - generator_config=GeneratorConfig( - ModelConfig( - vm_pu=1.0, - vmin_pu=0.80, - vmax_pu=1.15, - load_model=1, - collapse_swer=False, - calibration=False, - p_factor_base_exports=0.95, - p_factor_base_imports=0.90, - p_factor_forecast_pv=1.0, - fix_single_phase_loads=True, - max_single_phase_load=30000.0, - fix_overloading_consumers=True, - max_load_tx_ratio=3.0, - max_gen_tx_ratio=10.0, - fix_undersized_service_lines=True, - max_load_service_line_ratio=1.5, - max_load_lv_line_ratio=2.0, - collapse_lv_networks=False, - feeder_scenario_allocation_strategy=FeederScenarioAllocationStrategy.ADDITIVE, - closed_loop_v_reg_enabled=True, - closed_loop_v_reg_replace_all=True, - closed_loop_v_reg_set_point=0.985, - closed_loop_v_band=2.0, - closed_loop_time_delay=100, - closed_loop_v_limit=1.1, - default_tap_changer_time_delay=100, - default_tap_changer_set_point_pu=1.0, - default_tap_changer_band=2.0, - split_phase_default_load_loss_percentage=0.4, - split_phase_lv_kv=0.25, - swer_voltage_to_line_voltage= [ - [230, 400], - [240, 415], - [250, 433], - [6350, 11000], - [6400, 11000], - [12700, 22000], - [19100, 33000] - ], - load_placement=LoadPlacement.PER_USAGE_POINT, - load_interval_length_hours=0.5, - meter_placement_config=MeterPlacementConfig( - True, - True, - [SwitchMeterPlacementConfig(SwitchClass.LOAD_BREAK_SWITCH, ".*")], - "meter group 1"), - seed=42, - default_load_watts=[100.0, 200.0, 300.0], - default_gen_watts=[50.0, 150.0, 250.0], - default_load_var=[10.0, 20.0, 30.0], - default_gen_var=[5.0, 15.0, 25.0], - transformer_tap_settings="tap-3" - ), - SolveConfig( - norm_vmin_pu=0.9, - norm_vmax_pu=1.054, - emerg_vmin_pu=0.8, - emerg_vmax_pu=1.1, - base_frequency=50, - voltage_bases=[0.4, 0.433, 6.6, 11.0, 22.0, 33.0, 66.0, 132.0], - max_iter=25, - max_control_iter=20, - mode=SolveMode.YEARLY, - step_size_minutes=60 - ), - RawResultsConfig( - energy_meter_voltages_raw=True, - energy_meters_raw=True, - results_per_meter=True, - overloads_raw=True, - voltage_exceptions_raw=True - ) - ), - is_public=True) + scenario="scenario1", + year=2024, + feeder="feeder1", + time_zone=timezone(timedelta(hours=10)), + load_time=TimePeriod( + datetime(2022, 4, 1), + datetime(2023, 4, 1) + ), + model_name="TEST OPENDSS MODEL 1", + generator_config=GeneratorConfig( + ModelConfig( + vm_pu=1.0, + vmin_pu=0.80, + vmax_pu=1.15, + load_model=1, + collapse_swer=False, + calibration=False, + p_factor_base_exports=0.95, + p_factor_base_imports=0.90, + p_factor_forecast_pv=1.0, + fix_single_phase_loads=True, + max_single_phase_load=30000.0, + fix_overloading_consumers=True, + max_load_tx_ratio=3.0, + max_gen_tx_ratio=10.0, + fix_undersized_service_lines=True, + max_load_service_line_ratio=1.5, + max_load_lv_line_ratio=2.0, + collapse_lv_networks=False, + feeder_scenario_allocation_strategy=FeederScenarioAllocationStrategy.ADDITIVE, + closed_loop_v_reg_enabled=True, + closed_loop_v_reg_replace_all=True, + closed_loop_v_reg_set_point=0.985, + closed_loop_v_band=2.0, + closed_loop_time_delay=100, + closed_loop_v_limit=1.1, + default_tap_changer_time_delay=100, + default_tap_changer_set_point_pu=1.0, + default_tap_changer_band=2.0, + split_phase_default_load_loss_percentage=0.4, + split_phase_lv_kv=0.25, + swer_voltage_to_line_voltage=[ + [230, 400], + [240, 415], + [250, 433], + [6350, 11000], + [6400, 11000], + [12700, 22000], + [19100, 33000] + ], + load_placement=LoadPlacement.PER_USAGE_POINT, + load_interval_length_hours=0.5, + meter_placement_config=MeterPlacementConfig( + True, + True, + [SwitchMeterPlacementConfig(SwitchClass.LOAD_BREAK_SWITCH, ".*")], + "meter group 1"), + seed=42, + default_load_watts=[100.0, 200.0, 300.0], + default_gen_watts=[50.0, 150.0, 250.0], + default_load_var=[10.0, 20.0, 30.0], + default_gen_var=[5.0, 15.0, 25.0], + transformer_tap_settings="tap-3" + ), + SolveConfig( + norm_vmin_pu=0.9, + norm_vmax_pu=1.054, + emerg_vmin_pu=0.8, + emerg_vmax_pu=1.1, + base_frequency=50, + voltage_bases=[0.4, 0.433, 6.6, 11.0, 22.0, 33.0, 66.0, 132.0], + max_iter=25, + max_control_iter=20, + mode=SolveMode.YEARLY, + step_size_minutes=60 + ), + RawResultsConfig( + energy_meter_voltages_raw=True, + energy_meters_raw=True, + results_per_meter=True, + overloads_raw=True, + voltage_exceptions_raw=True + ) + ), + is_public=True) def test_run_opendss_export_no_verify_success(httpserver: HTTPServer): @@ -1170,7 +1171,7 @@ def get_paged_opendss_models_no_param_request_handler(request): query = " ".join(actual_body['query'].split()) assert query == " ".join(line.strip() for line in get_paged_opendss_models_query.strip().splitlines()) - assert actual_body['variables'] == { } + assert actual_body['variables'] == {} return Response(json.dumps({"result": "success"}), status=200, content_type="application/json")