diff --git a/changelog.md b/changelog.md index 3a01d63..ef6c0d8 100644 --- a/changelog.md +++ b/changelog.md @@ -13,12 +13,13 @@ * `rating_threshold` * `simplify_plsi_threshold` * `emerg_amp_scaling` +* Added optional field `inverterControlConfig` to `ModelConfig`. This `PVVoltVARVoltWattConfig` allows the configuration of advanced inverter control profiles. ### Enhancements * None. ### Fixes -* None. +* `TimePeriod` no longer truncates the `start_time` and `end_time` to midnight(`00:00:00`). `TimePeriod` will now preserve arbitrary start and end times to minute precision. ### Notes * None. diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 3a181b0..1808b66 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -244,7 +244,12 @@ def generator_config_to_json(self, generator_config: Optional[GeneratorConfig]) "useSpanLevelThreshold": generator_config.model.use_span_level_threshold, "ratingThreshold": generator_config.model.rating_threshold, "simplifyPLSIThreshold": generator_config.model.simplify_plsi_threshold, - "emergAmpScaling": generator_config.model.emerg_amp_scaling + "emergAmpScaling": generator_config.model.emerg_amp_scaling, + "inverterControlConfig": generator_config.model.inverter_control_config and { + "cutOffDate": generator_config.model.inverter_control_config.cut_off_date and generator_config.model.inverter_control_config.cut_off_date.isoformat(), + "beforeCutOffProfile": generator_config.model.inverter_control_config.beforeCutOffProfile, + "afterCutOffProfile": generator_config.model.inverter_control_config.afterCutOffProfile + } }, "solve": generator_config.solve and { "normVMinPu": generator_config.solve.norm_vmin_pu, @@ -1330,6 +1335,11 @@ async def async_get_paged_opendss_models( ratingThreshold simplifyPLSIThreshold emergAmpScaling + inverterControlConfig { + cutOffDate + beforeCutOffProfile + afterCutOffProfile + } } solve { normVMinPu diff --git a/src/zepben/eas/client/work_package.py b/src/zepben/eas/client/work_package.py index 11c746f..48051e4 100644 --- a/src/zepben/eas/client/work_package.py +++ b/src/zepben/eas/client/work_package.py @@ -21,6 +21,7 @@ "LoadPlacement", "FeederScenarioAllocationStrategy", "MeterPlacementConfig", + "PVVoltVARVoltWattConfig", "ModelConfig", "SolveMode", "SolveConfig", @@ -147,7 +148,7 @@ class FixedTime: """ def __init__(self, load_time: datetime, load_overrides: Optional[Dict[str, FixedTimeLoadOverride]] = None): - self.load_time = load_time.replace(tzinfo=None) + self.load_time = load_time.replace(second=0, microsecond=0, tzinfo=None) self.load_overrides = load_overrides @@ -165,8 +166,8 @@ def __init__( load_overrides: Optional[Dict[str, TimePeriodLoadOverride]] = None ): self._validate(start_time, end_time) - self.start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) - self.end_time = end_time.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) + self.start_time = start_time.replace(second=0, microsecond=0, tzinfo=None) + self.end_time = end_time.replace(second=0, microsecond=0, tzinfo=None) self.load_overrides = load_overrides @staticmethod @@ -208,6 +209,22 @@ class MeterPlacementConfig: """The ID of the meter group to use for populating EnergyMeters at EnergyConsumers.""" +@dataclass +class PVVoltVARVoltWattConfig: + cut_off_date: Optional[datetime] = None + """Optional cut-off date to determine which profile to apply to equipment during translation to the OpenDss model. + If supplied, the "commissionedDate" of the equipment is compared against this date, equipment that do not have a + "commissionedDate" will receive the [beforeCutOffProfile]. If null, the [afterCutOffProfile] profile is applied to all equipment.""" + + beforeCutOffProfile: Optional[str] = None + """Optional name of the profile to apply to equipment with a "commissionDate" before [cutOffDate]. + If null the equipment will be translated into a regular Generator the rather a PVSystem.""" + + afterCutOffProfile: Optional[str] = None + """Optional name of the profile to apply to equipment with a "commissionDate" after [cutOffDate]. + If null the equipment will be translated into a regular Generator the rather a PVSystem.""" + + @dataclass class ModelConfig: vm_pu: Optional[float] = None @@ -492,6 +509,11 @@ class ModelConfig: Set as a factor value, i.e put as 1.5 if scaling is 150% """ + inverter_control_config: Optional[PVVoltVARVoltWattConfig] = None + """ + Optional configuration object to enable modelling generation equipment as PVSystems controlled by InvControls rather than Generators. + """ + class SolveMode(Enum): YEARLY = "YEARLY" diff --git a/test/test_eas_client.py b/test/test_eas_client.py index ffde47a..641fab3 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -26,7 +26,7 @@ Order from zepben.eas.client.study import Result from zepben.eas.client.work_package import FeederConfigs, TimePeriodLoadOverride, \ - FixedTime, NodeLevelResultsConfig + FixedTime, NodeLevelResultsConfig, PVVoltVARVoltWattConfig from zepben.eas.client.work_package import WorkPackageConfig, TimePeriod, GeneratorConfig, ModelConfig, \ FeederScenarioAllocationStrategy, LoadPlacement, MeterPlacementConfig, SwitchMeterPlacementConfig, SwitchClass, \ SolveMode, RawResultsConfig @@ -195,8 +195,8 @@ def test_get_work_package_cost_estimation_no_verify_success(httpserver: HTTPServ [1], ["scenario"], TimePeriod( - datetime(2022, 1, 1), - datetime(2022, 1, 2), + datetime(2022, 1, 1, 10), + datetime(2022, 1, 2, 12), None ) ) @@ -753,6 +753,7 @@ def hosting_capacity_run_calibration_with_calibration_time_request_handler(reque 'fixUndersizedServiceLines': None, 'genVMaxPu': None, 'genVMinPu': None, + 'inverterControlConfig': None, 'loadIntervalLengthHours': None, 'loadModel': None, 'loadPlacement': None, @@ -860,6 +861,7 @@ def hosting_capacity_run_calibration_with_generator_config_request_handler(reque 'fixUndersizedServiceLines': None, 'genVMaxPu': None, 'genVMinPu': None, + 'inverterControlConfig': None, 'loadIntervalLengthHours': None, 'loadModel': None, 'loadPlacement': None, @@ -961,6 +963,7 @@ def hosting_capacity_run_calibration_with_partial_model_config_request_handler(r 'fixUndersizedServiceLines': None, 'genVMaxPu': None, 'genVMinPu': None, + 'inverterControlConfig': None, 'loadIntervalLengthHours': None, 'loadModel': None, 'loadPlacement': None, @@ -1089,8 +1092,8 @@ def run_opendss_export_request_handler(request): }] }} if isinstance(OPENDSS_CONFIG.load_time, FixedTime) else {"timePeriod": { - "startTime": "2022-04-01T00:00:00", - "endTime": "2023-04-01T00:00:00", + "startTime": "2022-04-01T10:13:00", + "endTime": "2023-04-01T12:14:00", "overrides": [{ 'loadId': 'meter1', 'loadWattsOverride': [1.0], @@ -1169,7 +1172,12 @@ def run_opendss_export_request_handler(request): "useSpanLevelThreshold": True, "ratingThreshold": 20.0, "simplifyPLSIThreshold": 20.0, - "emergAmpScaling": 1.8 + "emergAmpScaling": 1.8, + 'inverterControlConfig': { + 'afterCutOffProfile': 'afterProfile', + 'beforeCutOffProfile': 'beforeProfile', + 'cutOffDate': '2024-04-12T11:42:00' + }, }, "solve": { "normVMinPu": 0.9, @@ -1214,8 +1222,8 @@ def run_opendss_export_request_handler(request): year=2024, feeder="feeder1", load_time=TimePeriod( - datetime(2022, 4, 1), - datetime(2023, 4, 1), + datetime(2022, 4, 1, 10, 13), + datetime(2023, 4, 1, 12, 14), {"meter1": TimePeriodLoadOverride([1.0], [2.0], [3.0], [4.0])} ), model_name="TEST OPENDSS MODEL 1", @@ -1282,7 +1290,12 @@ def run_opendss_export_request_handler(request): use_span_level_threshold=True, rating_threshold=20.0, simplify_plsi_threshold=20.0, - emerg_amp_scaling= 1.8 + emerg_amp_scaling=1.8, + inverter_control_config=PVVoltVARVoltWattConfig( + cut_off_date=datetime(2024, 4, 12, 11, 42), + beforeCutOffProfile="beforeProfile", + afterCutOffProfile="afterProfile" + ) ), SolveConfig( norm_vmin_pu=0.9, @@ -1464,6 +1477,11 @@ def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver ratingThreshold simplifyPLSIThreshold emergAmpScaling + inverterControlConfig { + cutOffDate + beforeCutOffProfile + afterCutOffProfile + } } solve { normVMinPu