From 05048c7ff4c587759f2da3689f56a0896eb10941 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 7 Aug 2025 22:18:03 +1000 Subject: [PATCH 1/4] TODO:update tests and changelog Signed-off-by: vince --- src/zepben/eas/client/eas_client.py | 285 ++++++++++++++++++---------- 1 file changed, 189 insertions(+), 96 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 8eb9885..44ac70c 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -23,7 +23,8 @@ from zepben.eas.client.study import Study from zepben.eas.client.ingestor import IngestorConfigInput, IngestorRunsFilterInput, IngestorRunsSortCriteriaInput from zepben.eas.client.util import construct_url -from zepben.eas.client.work_package import WorkPackageConfig, FixedTime, TimePeriod, ForecastConfig, FeederConfigs +from zepben.eas.client.work_package import WorkPackageConfig, FixedTime, TimePeriod, ForecastConfig, FeederConfigs, \ + GeneratorConfig __all__ = ["EasClient"] @@ -34,20 +35,20 @@ class EasClient: """ def __init__( - self, - host: str, - port: int, - protocol: str = "https", - client_id: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - access_token: Optional[str] = None, - client_secret: Optional[str] = None, - token_fetcher: Optional[ZepbenTokenFetcher] = None, - verify_certificate: bool = True, - ca_filename: Optional[str] = None, - session: ClientSession = None, - json_serialiser=None + self, + host: str, + port: int, + protocol: str = "https", + client_id: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + access_token: Optional[str] = None, + client_secret: Optional[str] = None, + token_fetcher: Optional[ZepbenTokenFetcher] = None, + verify_certificate: bool = True, + ca_filename: Optional[str] = None, + session: ClientSession = None, + json_serialiser=None ): """ Construct a client for the Evolve App Server. If the server is HTTPS, authentication may be configured. @@ -421,10 +422,10 @@ async def async_get_work_package_cost_estimation(self, work_package: WorkPackage 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 + 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: return await response.json() @@ -654,10 +655,10 @@ async def async_run_hosting_capacity_work_package(self, work_package: WorkPackag 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 + 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: return await response.json() @@ -695,10 +696,10 @@ async def async_cancel_hosting_capacity_work_package(self, work_package_id: str) 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 + 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: return await response.json() @@ -746,10 +747,10 @@ async def async_get_hosting_capacity_work_packages_progress(self): 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 + 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: return await response.json() @@ -793,7 +794,7 @@ async def async_run_feeder_load_analysis_report(self, feeder_load_analysis_input "fetchLvNetwork": feeder_load_analysis_input.fetch_lv_network, "processFeederLoads": feeder_load_analysis_input.process_feeder_loads, "processCoincidentLoads": feeder_load_analysis_input.process_coincident_loads, - "produceConductorReport": True, # We currently only support conductor report + "produceConductorReport": True, # We currently only support conductor report "aggregateAtFeederLevel": feeder_load_analysis_input.aggregate_at_feeder_level, "output": feeder_load_analysis_input.output } @@ -803,10 +804,10 @@ async def async_run_feeder_load_analysis_report(self, feeder_load_analysis_input 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 + 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: return await response.json() @@ -1044,23 +1045,35 @@ async def async_get_ingestor_run_list(self, query_filter: Optional[IngestorRunsF else: raise response.raise_for_status() - def run_hosting_capacity_calibration(self, calibration_name: str, local_calibration_time: Optional[str] = None, feeders: Optional[List[str]] = None): + def run_hosting_capacity_calibration(self, calibration_name: str, local_calibration_time: datetime, + feeders: Optional[List[str]] = None, + generator_config: Optional[GeneratorConfig] = None): """ Send request to run hosting capacity calibration :param calibration_name: A string representation of the calibration name - :param local_calibration_time: A string representation of the calibration time, in model time. + :param local_calibration_time: A datetime representation of the calibration time, in the timezone of your pqv data ("model time"). :param feeders: A list of feeder ID's to run the calibration over. If not supplied then the calibration is run over all feeders in the network. + :param generator_config: A `GeneratorConfig` object that overrides the default values in the `WorkPackageConfig` used by calibration. + Note: The following fields cannot be overridden during calibration: generator_config.model.calibration, generator_config.model.meter_placement_config, generator_config.solve.step_size_minutes, and generator_config.raw_results. + + :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, feeders)) + self.async_run_hosting_capacity_calibration(calibration_name, local_calibration_time, feeders, + generator_config)) - async def async_run_hosting_capacity_calibration(self, calibration_name: str, calibration_time_local: datetime, feeders: Optional[List[str]] = None): + async def async_run_hosting_capacity_calibration(self, calibration_name: str, + calibration_time_local: datetime, + feeders: Optional[List[str]] = None, + generator_config: Optional[GeneratorConfig] = None): """ Send asynchronous request to run hosting capacity calibration :param calibration_name: A string representation of the calibration name - :param calibration_time_local: a datetime representation of the calibration time, in the timezone of your pqv data ("model time"). + :param calibration_time_local: A datetime representation of the calibration time, in the timezone of your pqv data ("model time"). :param feeders: A list of feeder ID's to run the calibration over. If not supplied then the calibration is run over all feeders in the network. + :param generator_config: A `GeneratorConfig` object that overrides the default values in the `WorkPackageConfig` used by calibration. + Note: The following fields cannot be overridden during calibration: generator_config.model.calibration, generator_config.model.meter_placement_config, generator_config.solve.step_size_minutes, and generator_config.raw_results. :return: The HTTP response received from the Evolve App Server after attempting to run the calibration """ @@ -1072,14 +1085,91 @@ async def async_run_hosting_capacity_calibration(self, calibration_name: str, ca warnings.filterwarnings("ignore", category=InsecureRequestWarning) json = { "query": """ - mutation runCalibration($calibrationName: String!, $calibrationTimeLocal: LocalDateTime, $feeders: [String!]) { - runCalibration(calibrationName: $calibrationName, calibrationTimeLocal: $calibrationTimeLocal, feeders: $feeders) + mutation runCalibration($calibrationName: String!, $calibrationTimeLocal: LocalDateTime, $feeders: [String!], $generatorConfig: HcGeneratorConfigInput) { + runCalibration(calibrationName: $calibrationName, calibrationTimeLocal: $calibrationTimeLocal, feeders: $feeders, generatorConfig: $generatorConfig) } """, "variables": { "calibrationName": calibration_name, "calibrationTimeLocal": parsed_time.isoformat(), - "feeders": feeders + "feeders": feeders, + "generatorConfig": generator_config and { + "model": generator_config.model and { + "vmPu": generator_config.model.vm_pu, + "loadVMinPu": generator_config.model.load_vmin_pu, + "loadVMaxPu": generator_config.model.load_vmax_pu, + "genVMinPu": generator_config.model.gen_vmin_pu, + "genVMaxPu": generator_config.model.gen_vmax_pu, + "loadModel": generator_config.model.load_model, + "collapseSWER": generator_config.model.collapse_swer, + "calibration": generator_config.model.calibration, + "pFactorBaseExports": generator_config.model.p_factor_base_exports, + "pFactorForecastPv": generator_config.model.p_factor_forecast_pv, + "pFactorBaseImports": generator_config.model.p_factor_base_imports, + "fixSinglePhaseLoads": generator_config.model.fix_single_phase_loads, + "maxSinglePhaseLoad": generator_config.model.max_single_phase_load, + "fixOverloadingConsumers": generator_config.model.fix_overloading_consumers, + "maxLoadTxRatio": generator_config.model.max_load_tx_ratio, + "maxGenTxRatio": generator_config.model.max_gen_tx_ratio, + "fixUndersizedServiceLines": generator_config.model.fix_undersized_service_lines, + "maxLoadServiceLineRatio": generator_config.model.max_load_service_line_ratio, + "maxLoadLvLineRatio": generator_config.model.max_load_lv_line_ratio, + "collapseLvNetworks": generator_config.model.collapse_lv_networks, + "feederScenarioAllocationStrategy": generator_config.model.feeder_scenario_allocation_strategy and generator_config.model.feeder_scenario_allocation_strategy.name, + "closedLoopVRegEnabled": generator_config.model.closed_loop_v_reg_enabled, + "closedLoopVRegReplaceAll": generator_config.model.closed_loop_v_reg_replace_all, + "closedLoopVRegSetPoint": generator_config.model.closed_loop_v_reg_set_point, + "closedLoopVBand": generator_config.model.closed_loop_v_band, + "closedLoopTimeDelay": generator_config.model.closed_loop_time_delay, + "closedLoopVLimit": generator_config.model.closed_loop_v_limit, + "defaultTapChangerTimeDelay": generator_config.model.default_tap_changer_time_delay, + "defaultTapChangerSetPointPu": generator_config.model.default_tap_changer_set_point_pu, + "defaultTapChangerBand": generator_config.model.default_tap_changer_band, + "splitPhaseDefaultLoadLossPercentage": generator_config.model.split_phase_default_load_loss_percentage, + "splitPhaseLVKV": generator_config.model.split_phase_lv_kv, + "swerVoltageToLineVoltage": generator_config.model.swer_voltage_to_line_voltage, + "loadPlacement": generator_config.model.load_placement and generator_config.model.load_placement.name, + "loadIntervalLengthHours": generator_config.model.load_interval_length_hours, + "meterPlacementConfig": generator_config.model.meter_placement_config and { + "feederHead": generator_config.model.meter_placement_config.feeder_head, + "distTransformers": generator_config.model.meter_placement_config.dist_transformers, + "switchMeterPlacementConfigs": 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 + generator_config.model.meter_placement_config.switch_meter_placement_configs + ], + "energyConsumerMeterGroup": generator_config.model.meter_placement_config.energy_consumer_meter_group + }, + "seed": generator_config.model.seed, + "defaultLoadWatts": generator_config.model.default_load_watts, + "defaultGenWatts": generator_config.model.default_gen_watts, + "defaultLoadVar": generator_config.model.default_load_var, + "defaultGenVar": generator_config.model.default_gen_var, + "transformerTapSettings": generator_config.model.transformer_tap_settings, + "ctPrimScalingFactor": generator_config.model.ct_prim_scaling_factor, + }, + "solve": generator_config.solve and { + "normVMinPu": generator_config.solve.norm_vmin_pu, + "normVMaxPu": generator_config.solve.norm_vmax_pu, + "emergVMinPu": generator_config.solve.emerg_vmin_pu, + "emergVMaxPu": generator_config.solve.emerg_vmax_pu, + "baseFrequency": generator_config.solve.base_frequency, + "voltageBases": generator_config.solve.voltage_bases, + "maxIter": generator_config.solve.max_iter, + "maxControlIter": generator_config.solve.max_control_iter, + "mode": generator_config.solve.mode and generator_config.solve.mode.name, + "stepSizeMinutes": generator_config.solve.step_size_minutes + }, + "rawResults": generator_config.raw_results and { + "energyMeterVoltagesRaw": generator_config.raw_results.energy_meter_voltages_raw, + "energyMetersRaw": generator_config.raw_results.energy_meters_raw, + "resultsPerMeter": generator_config.raw_results.results_per_meter, + "overloadsRaw": generator_config.raw_results.overloads_raw, + "voltageExceptionsRaw": generator_config.raw_results.voltage_exceptions_raw + } + } } } @@ -1087,10 +1177,10 @@ async def async_run_hosting_capacity_calibration(self, calibration_name: str, ca 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 + 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: return await response.json() @@ -1139,10 +1229,10 @@ async def async_get_hosting_capacity_calibration_run(self, id: str): 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 + 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: return await response.json() @@ -1175,24 +1265,27 @@ async def async_get_hosting_capacity_calibration_sets(self): 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 + 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: return await response.json() else: response.raise_for_status() - def get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, transformer_mrid: Optional[str] = None): + def get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, + transformer_mrid: Optional[str] = None): """ Retrieve distribution transformer tap settings from a calibration set in the hosting capacity input database. :return: The HTTP response received from the Evolve App Server after requesting transformer tap settings for the calibration id """ - return get_event_loop().run_until_complete(self.async_get_transformer_tap_settings(calibration_id, feeder, transformer_mrid)) + return get_event_loop().run_until_complete( + self.async_get_transformer_tap_settings(calibration_id, feeder, transformer_mrid)) - async def async_get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, transformer_mrid: Optional[str] = None): + async def async_get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, + transformer_mrid: Optional[str] = None): """ Retrieve distribution transformer tap settings from a calibration set in the hosting capacity input database. :return: The HTTP response received from the Evolve App Server after requesting transformer tap settings for the calibration id @@ -1224,10 +1317,10 @@ async def async_get_transformer_tap_settings(self, calibration_id: str, feeder: 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 + 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: return await response.json() @@ -1270,16 +1363,16 @@ async def async_run_opendss_export(self, config: OpenDssConfig): "modulesConfiguration": { "common": { **({"fixedTime": {"loadTime": config.load_time.load_time.isoformat(), - "overrides": config.load_time.load_overrides and [ - { - "loadId": key, - "loadWattsOverride": value.load_watts, - "genWattsOverride": value.gen_watts, - "loadVarOverride": value.load_var, - "genVarOverride": value.gen_var, - } for key, value in config.load_time.load_overrides.items() - ] - }} if isinstance(config.load_time, FixedTime) else {}), + "overrides": config.load_time.load_overrides and [ + { + "loadId": key, + "loadWattsOverride": value.load_watts, + "genWattsOverride": value.gen_watts, + "loadVarOverride": value.load_var, + "genVarOverride": value.gen_var, + } for key, value in config.load_time.load_overrides.items() + ] + }} if isinstance(config.load_time, FixedTime) else {}), **({"timePeriod": { "startTime": config.load_time.start_time.isoformat(), "endTime": config.load_time.end_time.isoformat(), @@ -1380,10 +1473,10 @@ async def async_run_opendss_export(self, config: OpenDssConfig): 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 + 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: return await response.json() @@ -1391,11 +1484,11 @@ async def async_run_opendss_export(self, config: OpenDssConfig): response.raise_for_status() def get_paged_opendss_models( - self, - limit: Optional[int] = None, - offset: Optional[int] = None, - query_filter: Optional[GetOpenDssModelsFilterInput] = None, - query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): + 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 @@ -1408,11 +1501,11 @@ def get_paged_opendss_models( 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): + 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 @@ -1569,10 +1662,10 @@ async def async_get_paged_opendss_models( 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 + 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: return await response.json() @@ -1601,11 +1694,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: return response.headers["Location"] From 50d769c8e59edc3db84e25288bb11216df85b246 Mon Sep 17 00:00:00 2001 From: vince Date: Wed, 13 Aug 2025 11:53:41 +1000 Subject: [PATCH 2/4] updated tests Signed-off-by: vince --- changelog.md | 6 +++- test/test_eas_client.py | 80 +++++++++++++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/changelog.md b/changelog.md index ad7e680..c3dea15 100644 --- a/changelog.md +++ b/changelog.md @@ -8,7 +8,11 @@ * Added basic client methods `get_ingestor_run` and `get_ingestor_run_list` to retrieve the records of previous ingestor runs. ### Enhancements -* None. + +* Added optional `generator_config` argument to `run_hosting_capacity_calibration`. This allows the user to override the + default values in the `WorkPackageConfig` used by calibration. Note: The following fields cannot be overridden during + calibration: GeneratorConfig.model.calibration, GeneratorConfig.model.meter_placement_config, + GeneratorConfig.solve.step_size_minutes, and GeneratorConfig.raw_results. ### Fixes * None. diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 65e96ec..814c5c0 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -611,8 +611,11 @@ def hosting_capacity_run_calibration_request_handler(request): actual_body = json.loads(request.data.decode()) query = " ".join(actual_body['query'].split()) - assert query == "mutation runCalibration($calibrationName: String!, $calibrationTimeLocal: LocalDateTime, $feeders: [String!]) { runCalibration(calibrationName: $calibrationName, calibrationTimeLocal: $calibrationTimeLocal, feeders: $feeders) }" - assert actual_body['variables'] == {"calibrationName": "TEST CALIBRATION", "calibrationTimeLocal": datetime(2025, month=7, day=12).isoformat(), "feeders": None} + assert query == "mutation runCalibration($calibrationName: String!, $calibrationTimeLocal: LocalDateTime, $feeders: [String!], $generatorConfig: HcGeneratorConfigInput) { runCalibration(calibrationName: $calibrationName, calibrationTimeLocal: $calibrationTimeLocal, feeders: $feeders, generatorConfig: $generatorConfig) }" + assert actual_body['variables'] == {"calibrationName": "TEST CALIBRATION", + "calibrationTimeLocal": datetime(2025, month=7, day=12).isoformat(), + "feeders": None, 'generatorConfig': None + } return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") @@ -624,7 +627,8 @@ def test_run_hosting_capacity_calibration_no_verify_success(httpserver: HTTPServ verify_certificate=False ) - httpserver.expect_oneshot_request("/api/graphql").respond_with_handler(hosting_capacity_run_calibration_request_handler) + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + hosting_capacity_run_calibration_request_handler) res = eas_client.run_hosting_capacity_calibration("TEST CALIBRATION", datetime(2025, month=7, day=12)) httpserver.check_assertions() assert res == {"result": "success"} @@ -718,10 +722,57 @@ def hosting_capacity_run_calibration_with_calibration_time_request_handler(reque actual_body = json.loads(request.data.decode()) query = " ".join(actual_body['query'].split()) - assert query == "mutation runCalibration($calibrationName: String!, $calibrationTimeLocal: LocalDateTime, $feeders: [String!]) { runCalibration(calibrationName: $calibrationName, calibrationTimeLocal: $calibrationTimeLocal, feeders: $feeders) }" + assert query == "mutation runCalibration($calibrationName: String!, $calibrationTimeLocal: LocalDateTime, $feeders: [String!], $generatorConfig: HcGeneratorConfigInput) { runCalibration(calibrationName: $calibrationName, calibrationTimeLocal: $calibrationTimeLocal, feeders: $feeders, generatorConfig: $generatorConfig) }" assert actual_body['variables'] == {"calibrationName": "TEST CALIBRATION", - "calibrationTimeLocal": datetime(1902, month=1, day=28, hour=0, minute=0, second=20).isoformat(), - "feeders": ["one", "two"]} + "calibrationTimeLocal": datetime(1902, month=1, day=28, hour=0, minute=0, + second=20).isoformat(), + "feeders": ["one", "two"], + "generatorConfig": {'model': {'calibration': None, + 'closedLoopTimeDelay': None, + 'closedLoopVBand': None, + 'closedLoopVLimit': None, + 'closedLoopVRegEnabled': None, + 'closedLoopVRegReplaceAll': None, + 'closedLoopVRegSetPoint': None, + 'collapseLvNetworks': None, + 'collapseSWER': None, + 'ctPrimScalingFactor': None, + 'defaultGenVar': None, + 'defaultGenWatts': None, + 'defaultLoadVar': None, + 'defaultLoadWatts': None, + 'defaultTapChangerBand': None, + 'defaultTapChangerSetPointPu': None, + 'defaultTapChangerTimeDelay': None, + 'feederScenarioAllocationStrategy': None, + 'fixOverloadingConsumers': None, + 'fixSinglePhaseLoads': None, + 'fixUndersizedServiceLines': None, + 'genVMaxPu': None, + 'genVMinPu': None, + 'loadIntervalLengthHours': None, + 'loadModel': None, + 'loadPlacement': None, + 'loadVMaxPu': None, + 'loadVMinPu': None, + 'maxGenTxRatio': None, + 'maxLoadLvLineRatio': None, + 'maxLoadServiceLineRatio': None, + 'maxLoadTxRatio': None, + 'maxSinglePhaseLoad': None, + 'meterPlacementConfig': None, + 'pFactorBaseExports': None, + 'pFactorBaseImports': None, + 'pFactorForecastPv': None, + 'seed': None, + 'splitPhaseDefaultLoadLossPercentage': None, + 'splitPhaseLVKV': None, + 'swerVoltageToLineVoltage': None, + 'transformerTapSettings': 'test_tap_settings', + 'vmPu': None}, + 'rawResults': None, + 'solve': None} + } return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") @@ -733,8 +784,15 @@ def test_run_hosting_capacity_calibration_with_calibration_time_no_verify_succes verify_certificate=False ) - httpserver.expect_oneshot_request("/api/graphql").respond_with_handler(hosting_capacity_run_calibration_with_calibration_time_request_handler) - res = eas_client.run_hosting_capacity_calibration("TEST CALIBRATION", datetime(1902, month=1, day=28, hour=0, minute=0, second=20), ["one", "two"]) + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + hosting_capacity_run_calibration_with_calibration_time_request_handler) + res = eas_client.run_hosting_capacity_calibration("TEST CALIBRATION", + datetime(1902, month=1, day=28, hour=0, minute=0, second=20), + ["one", "two"], + generator_config=GeneratorConfig(model=ModelConfig( + transformer_tap_settings="test_tap_settings" + )) + ) httpserver.check_assertions() assert res == {"result": "success"} @@ -1022,7 +1080,8 @@ def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver ca_filename=ca_filename ) - OPENDSS_CONFIG.load_time = FixedTime(datetime(2022, 4, 1), {"meter1": FixedTimeLoadOverride([1.0], [2.0], [3.0], [4.0])}) + OPENDSS_CONFIG.load_time = FixedTime(datetime(2022, 4, 1), + {"meter1": FixedTimeLoadOverride([1.0], [2.0], [3.0], [4.0])}) httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( run_opendss_export_request_handler) res = eas_client.run_opendss_export(OPENDSS_CONFIG) @@ -1418,7 +1477,8 @@ def test_get_ingestor_run_list_all_filters_no_verify_success(httpserver: HTTPSer verify_certificate=False ) - httpserver.expect_oneshot_request("/api/graphql").respond_with_handler(get_ingestor_run_list_request_complete_handler) + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + get_ingestor_run_list_request_complete_handler) res = eas_client.get_ingestor_run_list( query_filter=IngestorRunsFilterInput( id=4, From dda05c9f26ab27d993c2593ba83706e7c88f473e Mon Sep 17 00:00:00 2001 From: vince Date: Wed, 13 Aug 2025 13:11:19 +1000 Subject: [PATCH 3/4] undo formatting Signed-off-by: vince --- src/zepben/eas/client/eas_client.py | 177 ++++++++++++++-------------- test/test_eas_client.py | 9 +- 2 files changed, 90 insertions(+), 96 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 44ac70c..a2952dd 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -35,20 +35,20 @@ class EasClient: """ def __init__( - self, - host: str, - port: int, - protocol: str = "https", - client_id: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - access_token: Optional[str] = None, - client_secret: Optional[str] = None, - token_fetcher: Optional[ZepbenTokenFetcher] = None, - verify_certificate: bool = True, - ca_filename: Optional[str] = None, - session: ClientSession = None, - json_serialiser=None + self, + host: str, + port: int, + protocol: str = "https", + client_id: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + access_token: Optional[str] = None, + client_secret: Optional[str] = None, + token_fetcher: Optional[ZepbenTokenFetcher] = None, + verify_certificate: bool = True, + ca_filename: Optional[str] = None, + session: ClientSession = None, + json_serialiser=None ): """ Construct a client for the Evolve App Server. If the server is HTTPS, authentication may be configured. @@ -422,10 +422,10 @@ async def async_get_work_package_cost_estimation(self, work_package: WorkPackage 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 + 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: return await response.json() @@ -655,10 +655,10 @@ async def async_run_hosting_capacity_work_package(self, work_package: WorkPackag 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 + 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: return await response.json() @@ -696,10 +696,10 @@ async def async_cancel_hosting_capacity_work_package(self, work_package_id: str) 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 + 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: return await response.json() @@ -747,10 +747,10 @@ async def async_get_hosting_capacity_work_packages_progress(self): 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 + 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: return await response.json() @@ -794,7 +794,7 @@ async def async_run_feeder_load_analysis_report(self, feeder_load_analysis_input "fetchLvNetwork": feeder_load_analysis_input.fetch_lv_network, "processFeederLoads": feeder_load_analysis_input.process_feeder_loads, "processCoincidentLoads": feeder_load_analysis_input.process_coincident_loads, - "produceConductorReport": True, # We currently only support conductor report + "produceConductorReport": True, # We currently only support conductor report "aggregateAtFeederLevel": feeder_load_analysis_input.aggregate_at_feeder_level, "output": feeder_load_analysis_input.output } @@ -804,10 +804,10 @@ async def async_run_feeder_load_analysis_report(self, feeder_load_analysis_input 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 + 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: return await response.json() @@ -1177,10 +1177,10 @@ async def async_run_hosting_capacity_calibration(self, calibration_name: str, 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 + 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: return await response.json() @@ -1229,10 +1229,10 @@ async def async_get_hosting_capacity_calibration_run(self, id: str): 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 + 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: return await response.json() @@ -1265,27 +1265,24 @@ async def async_get_hosting_capacity_calibration_sets(self): 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 + 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: return await response.json() else: response.raise_for_status() - def get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, - transformer_mrid: Optional[str] = None): + def get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, transformer_mrid: Optional[str] = None): """ Retrieve distribution transformer tap settings from a calibration set in the hosting capacity input database. :return: The HTTP response received from the Evolve App Server after requesting transformer tap settings for the calibration id """ - return get_event_loop().run_until_complete( - self.async_get_transformer_tap_settings(calibration_id, feeder, transformer_mrid)) + return get_event_loop().run_until_complete(self.async_get_transformer_tap_settings(calibration_id, feeder, transformer_mrid)) - async def async_get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, - transformer_mrid: Optional[str] = None): + async def async_get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, transformer_mrid: Optional[str] = None): """ Retrieve distribution transformer tap settings from a calibration set in the hosting capacity input database. :return: The HTTP response received from the Evolve App Server after requesting transformer tap settings for the calibration id @@ -1317,10 +1314,10 @@ async def async_get_transformer_tap_settings(self, calibration_id: str, feeder: 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 + 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: return await response.json() @@ -1363,16 +1360,16 @@ async def async_run_opendss_export(self, config: OpenDssConfig): "modulesConfiguration": { "common": { **({"fixedTime": {"loadTime": config.load_time.load_time.isoformat(), - "overrides": config.load_time.load_overrides and [ - { - "loadId": key, - "loadWattsOverride": value.load_watts, - "genWattsOverride": value.gen_watts, - "loadVarOverride": value.load_var, - "genVarOverride": value.gen_var, - } for key, value in config.load_time.load_overrides.items() - ] - }} if isinstance(config.load_time, FixedTime) else {}), + "overrides": config.load_time.load_overrides and [ + { + "loadId": key, + "loadWattsOverride": value.load_watts, + "genWattsOverride": value.gen_watts, + "loadVarOverride": value.load_var, + "genVarOverride": value.gen_var, + } for key, value in config.load_time.load_overrides.items() + ] + }} if isinstance(config.load_time, FixedTime) else {}), **({"timePeriod": { "startTime": config.load_time.start_time.isoformat(), "endTime": config.load_time.end_time.isoformat(), @@ -1473,10 +1470,10 @@ async def async_run_opendss_export(self, config: OpenDssConfig): 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 + 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: return await response.json() @@ -1484,11 +1481,11 @@ async def async_run_opendss_export(self, config: OpenDssConfig): response.raise_for_status() def get_paged_opendss_models( - self, - limit: Optional[int] = None, - offset: Optional[int] = None, - query_filter: Optional[GetOpenDssModelsFilterInput] = None, - query_sort: Optional[GetOpenDssModelsSortCriteriaInput] = None): + 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 @@ -1501,11 +1498,11 @@ def get_paged_opendss_models( 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): + 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 @@ -1662,10 +1659,10 @@ async def async_get_paged_opendss_models( 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 + 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: return await response.json() @@ -1694,11 +1691,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: return response.headers["Location"] diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 814c5c0..e5a659b 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -627,8 +627,7 @@ def test_run_hosting_capacity_calibration_no_verify_success(httpserver: HTTPServ verify_certificate=False ) - httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( - hosting_capacity_run_calibration_request_handler) + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler(hosting_capacity_run_calibration_request_handler) res = eas_client.run_hosting_capacity_calibration("TEST CALIBRATION", datetime(2025, month=7, day=12)) httpserver.check_assertions() assert res == {"result": "success"} @@ -1080,8 +1079,7 @@ def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver ca_filename=ca_filename ) - OPENDSS_CONFIG.load_time = FixedTime(datetime(2022, 4, 1), - {"meter1": FixedTimeLoadOverride([1.0], [2.0], [3.0], [4.0])}) + OPENDSS_CONFIG.load_time = FixedTime(datetime(2022, 4, 1), {"meter1": FixedTimeLoadOverride([1.0], [2.0], [3.0], [4.0])}) httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( run_opendss_export_request_handler) res = eas_client.run_opendss_export(OPENDSS_CONFIG) @@ -1477,8 +1475,7 @@ def test_get_ingestor_run_list_all_filters_no_verify_success(httpserver: HTTPSer verify_certificate=False ) - httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( - get_ingestor_run_list_request_complete_handler) + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler(get_ingestor_run_list_request_complete_handler) res = eas_client.get_ingestor_run_list( query_filter=IngestorRunsFilterInput( id=4, From f0aa88c577363ab2502a36547e8cd8d0d68676a5 Mon Sep 17 00:00:00 2001 From: vince Date: Wed, 13 Aug 2025 14:00:19 +1000 Subject: [PATCH 4/4] pull out transformer tap settings Signed-off-by: vince --- changelog.md | 3 + src/zepben/eas/client/eas_client.py | 33 ++++- test/test_eas_client.py | 208 ++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index c3dea15..20c4498 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,9 @@ default values in the `WorkPackageConfig` used by calibration. Note: The following fields cannot be overridden during calibration: GeneratorConfig.model.calibration, GeneratorConfig.model.meter_placement_config, GeneratorConfig.solve.step_size_minutes, and GeneratorConfig.raw_results. +* Added optional `transformer_tap_settings` argument to `run_hosting_capacity_calibration`. This is the equivalent to supplying a `generator_config` + with `generator_config.model.transformer_tap_settings` set. If `transformer_tap_settings` is supplied, it will take precedence over any `transformer_tap_settings` + configured in the `generator_config`. ### Fixes * None. diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index a2952dd..1dee151 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -24,7 +24,7 @@ from zepben.eas.client.ingestor import IngestorConfigInput, IngestorRunsFilterInput, IngestorRunsSortCriteriaInput from zepben.eas.client.util import construct_url from zepben.eas.client.work_package import WorkPackageConfig, FixedTime, TimePeriod, ForecastConfig, FeederConfigs, \ - GeneratorConfig + GeneratorConfig, ModelConfig __all__ = ["EasClient"] @@ -1047,39 +1047,57 @@ async def async_get_ingestor_run_list(self, query_filter: Optional[IngestorRunsF def run_hosting_capacity_calibration(self, calibration_name: str, local_calibration_time: datetime, feeders: Optional[List[str]] = None, + transformer_tap_settings: Optional[str] = None, generator_config: Optional[GeneratorConfig] = None): """ Send request to run hosting capacity calibration :param calibration_name: A string representation of the calibration name :param local_calibration_time: A datetime representation of the calibration time, in the timezone of your pqv data ("model time"). :param feeders: A list of feeder ID's to run the calibration over. If not supplied then the calibration is run over all feeders in the network. + :param transformer_tap_settings: A set of transformer tap settings to apply before running the calibration work package. + If provided, this will take precedence over any 'transformer_tap_settings' supplied in via the generator_config parameter :param generator_config: A `GeneratorConfig` object that overrides the default values in the `WorkPackageConfig` used by calibration. - Note: The following fields cannot be overridden during calibration: generator_config.model.calibration, generator_config.model.meter_placement_config, generator_config.solve.step_size_minutes, and generator_config.raw_results. + Note: The following fields cannot be overridden during calibration: generator_config.model.calibration, generator_config.model.meter_placement_config, generator_config.solve.step_size_minutes, and generator_config.raw_results. :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, feeders, + transformer_tap_settings, generator_config)) async def async_run_hosting_capacity_calibration(self, calibration_name: str, calibration_time_local: datetime, feeders: Optional[List[str]] = None, + transformer_tap_settings: Optional[str] = None, generator_config: Optional[GeneratorConfig] = None): """ Send asynchronous request to run hosting capacity calibration :param calibration_name: A string representation of the calibration name :param calibration_time_local: A datetime representation of the calibration time, in the timezone of your pqv data ("model time"). :param feeders: A list of feeder ID's to run the calibration over. If not supplied then the calibration is run over all feeders in the network. + :param transformer_tap_settings: A set of transformer tap settings to apply before running the calibration work package. + If provided, this will take precedence over any 'transformer_tap_settings' supplied in via the generator_config parameter :param generator_config: A `GeneratorConfig` object that overrides the default values in the `WorkPackageConfig` used by calibration. - Note: The following fields cannot be overridden during calibration: generator_config.model.calibration, generator_config.model.meter_placement_config, generator_config.solve.step_size_minutes, and generator_config.raw_results. + Note: The following fields cannot be overridden during calibration: generator_config.model.calibration, generator_config.model.meter_placement_config, generator_config.solve.step_size_minutes, and generator_config.raw_results. + :return: The HTTP response received from the Evolve App Server after attempting to run the calibration """ # Only replace microsecond, as in database we only have down to second precision. # tzinfo will be whatever the user passed through, which should be the timezone of their load data. parsed_time = calibration_time_local.replace(microsecond=0, tzinfo=None) + + if transformer_tap_settings: + if generator_config: + if generator_config.model: + generator_config.model.transformer_tap_settings = transformer_tap_settings + else: + generator_config.model = ModelConfig(transformer_tap_settings=transformer_tap_settings) + else: + generator_config = GeneratorConfig(model=ModelConfig(transformer_tap_settings=transformer_tap_settings)) + with warnings.catch_warnings(): if not self._verify_certificate: warnings.filterwarnings("ignore", category=InsecureRequestWarning) @@ -1275,14 +1293,17 @@ async def async_get_hosting_capacity_calibration_sets(self): else: response.raise_for_status() - def get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, transformer_mrid: Optional[str] = None): + def get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, + transformer_mrid: Optional[str] = None): """ Retrieve distribution transformer tap settings from a calibration set in the hosting capacity input database. :return: The HTTP response received from the Evolve App Server after requesting transformer tap settings for the calibration id """ - return get_event_loop().run_until_complete(self.async_get_transformer_tap_settings(calibration_id, feeder, transformer_mrid)) + return get_event_loop().run_until_complete( + self.async_get_transformer_tap_settings(calibration_id, feeder, transformer_mrid)) - async def async_get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, transformer_mrid: Optional[str] = None): + async def async_get_transformer_tap_settings(self, calibration_id: str, feeder: Optional[str] = None, + transformer_mrid: Optional[str] = None): """ Retrieve distribution transformer tap settings from a calibration set in the hosting capacity input database. :return: The HTTP response received from the Evolve App Server after requesting transformer tap settings for the calibration id diff --git a/test/test_eas_client.py b/test/test_eas_client.py index e5a659b..b37c87f 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -796,6 +796,214 @@ def test_run_hosting_capacity_calibration_with_calibration_time_no_verify_succes assert res == {"result": "success"} +def test_run_hosting_capacity_calibration_with_explicit_transformer_tap_settings_no_generator_config(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + hosting_capacity_run_calibration_with_calibration_time_request_handler) + res = eas_client.run_hosting_capacity_calibration("TEST CALIBRATION", + datetime(1902, month=1, day=28, hour=0, minute=0, second=20), + ["one", "two"], + transformer_tap_settings="test_tap_settings" + ) + httpserver.check_assertions() + assert res == {"result": "success"} + + +def hosting_capacity_run_calibration_with_generator_config_request_handler(request): + actual_body = json.loads(request.data.decode()) + query = " ".join(actual_body['query'].split()) + + assert query == "mutation runCalibration($calibrationName: String!, $calibrationTimeLocal: LocalDateTime, $feeders: [String!], $generatorConfig: HcGeneratorConfigInput) { runCalibration(calibrationName: $calibrationName, calibrationTimeLocal: $calibrationTimeLocal, feeders: $feeders, generatorConfig: $generatorConfig) }" + assert actual_body['variables'] == {"calibrationName": "TEST CALIBRATION", + "calibrationTimeLocal": datetime(1902, month=1, day=28, hour=0, minute=0, + second=20).isoformat(), + "feeders": ["one", "two"], + "generatorConfig": {'model': {'calibration': None, + 'closedLoopTimeDelay': None, + 'closedLoopVBand': None, + 'closedLoopVLimit': None, + 'closedLoopVRegEnabled': None, + 'closedLoopVRegReplaceAll': None, + 'closedLoopVRegSetPoint': None, + 'collapseLvNetworks': None, + 'collapseSWER': None, + 'ctPrimScalingFactor': None, + 'defaultGenVar': None, + 'defaultGenWatts': None, + 'defaultLoadVar': None, + 'defaultLoadWatts': None, + 'defaultTapChangerBand': None, + 'defaultTapChangerSetPointPu': None, + 'defaultTapChangerTimeDelay': None, + 'feederScenarioAllocationStrategy': None, + 'fixOverloadingConsumers': None, + 'fixSinglePhaseLoads': None, + 'fixUndersizedServiceLines': None, + 'genVMaxPu': None, + 'genVMinPu': None, + 'loadIntervalLengthHours': None, + 'loadModel': None, + 'loadPlacement': None, + 'loadVMaxPu': None, + 'loadVMinPu': None, + 'maxGenTxRatio': None, + 'maxLoadLvLineRatio': None, + 'maxLoadServiceLineRatio': None, + 'maxLoadTxRatio': None, + 'maxSinglePhaseLoad': None, + 'meterPlacementConfig': None, + 'pFactorBaseExports': None, + 'pFactorBaseImports': None, + 'pFactorForecastPv': None, + 'seed': None, + 'splitPhaseDefaultLoadLossPercentage': None, + 'splitPhaseLVKV': None, + 'swerVoltageToLineVoltage': None, + 'transformerTapSettings': 'test_tap_settings', + 'vmPu': None + }, + 'rawResults': None, + 'solve': {'baseFrequency': None, + 'emergVMaxPu': None, + 'emergVMinPu': None, + 'maxControlIter': None, + 'maxIter': None, + 'mode': None, + 'normVMaxPu': 23.9, + 'normVMinPu': None, + 'stepSizeMinutes': None, + 'voltageBases': None + } + } + } + + return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") + + +def test_run_hosting_capacity_calibration_with_explicit_transformer_tap_settings_partial_generator_config(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + hosting_capacity_run_calibration_with_generator_config_request_handler) + res = eas_client.run_hosting_capacity_calibration("TEST CALIBRATION", + datetime(1902, month=1, day=28, hour=0, minute=0, second=20), + ["one", "two"], + transformer_tap_settings="test_tap_settings", + generator_config=GeneratorConfig(solve=SolveConfig(norm_vmax_pu=23.9)) + ) + httpserver.check_assertions() + assert res == {"result": "success"} + + +def hosting_capacity_run_calibration_with_partial_model_config_request_handler(request): + actual_body = json.loads(request.data.decode()) + query = " ".join(actual_body['query'].split()) + + assert query == "mutation runCalibration($calibrationName: String!, $calibrationTimeLocal: LocalDateTime, $feeders: [String!], $generatorConfig: HcGeneratorConfigInput) { runCalibration(calibrationName: $calibrationName, calibrationTimeLocal: $calibrationTimeLocal, feeders: $feeders, generatorConfig: $generatorConfig) }" + assert actual_body['variables'] == {"calibrationName": "TEST CALIBRATION", + "calibrationTimeLocal": datetime(1902, month=1, day=28, hour=0, minute=0, + second=20).isoformat(), + "feeders": ["one", "two"], + "generatorConfig": {'model': {'calibration': None, + 'closedLoopTimeDelay': None, + 'closedLoopVBand': None, + 'closedLoopVLimit': None, + 'closedLoopVRegEnabled': None, + 'closedLoopVRegReplaceAll': None, + 'closedLoopVRegSetPoint': None, + 'collapseLvNetworks': None, + 'collapseSWER': None, + 'ctPrimScalingFactor': None, + 'defaultGenVar': None, + 'defaultGenWatts': None, + 'defaultLoadVar': None, + 'defaultLoadWatts': None, + 'defaultTapChangerBand': None, + 'defaultTapChangerSetPointPu': None, + 'defaultTapChangerTimeDelay': None, + 'feederScenarioAllocationStrategy': None, + 'fixOverloadingConsumers': None, + 'fixSinglePhaseLoads': None, + 'fixUndersizedServiceLines': None, + 'genVMaxPu': None, + 'genVMinPu': None, + 'loadIntervalLengthHours': None, + 'loadModel': None, + 'loadPlacement': None, + 'loadVMaxPu': None, + 'loadVMinPu': None, + 'maxGenTxRatio': None, + 'maxLoadLvLineRatio': None, + 'maxLoadServiceLineRatio': None, + 'maxLoadTxRatio': None, + 'maxSinglePhaseLoad': None, + 'meterPlacementConfig': None, + 'pFactorBaseExports': None, + 'pFactorBaseImports': None, + 'pFactorForecastPv': None, + 'seed': None, + 'splitPhaseDefaultLoadLossPercentage': None, + 'splitPhaseLVKV': None, + 'swerVoltageToLineVoltage': None, + 'transformerTapSettings': 'test_tap_settings', + 'vmPu': 123.4 + }, + 'rawResults': None, + 'solve': None + } + } + + return Response(json.dumps({"result": "success"}), status=200, content_type="application/json") + + +def test_run_hosting_capacity_calibration_with_explicit_transformer_tap_settings_partial_model_config(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + hosting_capacity_run_calibration_with_partial_model_config_request_handler) + res = eas_client.run_hosting_capacity_calibration("TEST CALIBRATION", + datetime(1902, month=1, day=28, hour=0, minute=0, second=20), + ["one", "two"], + transformer_tap_settings="test_tap_settings", + generator_config=GeneratorConfig(model=ModelConfig(vm_pu=123.4)) + ) + httpserver.check_assertions() + assert res == {"result": "success"} + + +def test_run_hosting_capacity_calibration_with_explicit_transformer_tap_settings(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + httpserver.expect_oneshot_request("/api/graphql").respond_with_handler( + hosting_capacity_run_calibration_with_calibration_time_request_handler) + res = eas_client.run_hosting_capacity_calibration("TEST CALIBRATION", + datetime(1902, month=1, day=28, hour=0, minute=0, second=20), + ["one", "two"], + transformer_tap_settings="test_tap_settings", + generator_config=GeneratorConfig(model=ModelConfig( + transformer_tap_settings="this_should_be_over_written" + )) + ) + httpserver.check_assertions() + assert res == {"result": "success"} + def get_hosting_capacity_calibration_sets_request_handler(request): actual_body = json.loads(request.data.decode()) query = " ".join(actual_body['query'].split())