From 6c8bd28c1e681910cb131a24fc4a0aa270526077 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 25 Sep 2025 15:34:11 +1000 Subject: [PATCH 1/5] one Signed-off-by: vince --- src/zepben/eas/client/work_package.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/zepben/eas/client/work_package.py b/src/zepben/eas/client/work_package.py index 11c746f..f78b3b9 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", @@ -208,6 +209,23 @@ 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] = "default" + """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 +510,11 @@ class ModelConfig: Set as a factor value, i.e put as 1.5 if scaling is 150% """ + inverterControlConfig: Optional[PVVoltVARVoltWattConfig] = None + """ + Optional configuration object to enable modelling generation equipment as PVSystems controlled by InvControls rather than Generators. + """ + class SolveMode(Enum): YEARLY = "YEARLY" From f9a4b503b95705ad50f474d291e6493d79d0d179 Mon Sep 17 00:00:00 2001 From: vince Date: Mon, 29 Sep 2025 16:16:31 +1000 Subject: [PATCH 2/5] dunno Signed-off-by: vince --- src/zepben/eas/client/eas_client.py | 12 +++++++++++- src/zepben/eas/client/work_package.py | 3 +-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 3a181b0..0484d3c 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.replace(microsecond=0, tzinfo=None).isoformat(), # remove any timezone information + "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 f78b3b9..9895083 100644 --- a/src/zepben/eas/client/work_package.py +++ b/src/zepben/eas/client/work_package.py @@ -216,7 +216,6 @@ class PVVoltVARVoltWattConfig: 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.""" @@ -510,7 +509,7 @@ class ModelConfig: Set as a factor value, i.e put as 1.5 if scaling is 150% """ - inverterControlConfig: Optional[PVVoltVARVoltWattConfig] = None + inverter_control_config: Optional[PVVoltVARVoltWattConfig] = None """ Optional configuration object to enable modelling generation equipment as PVSystems controlled by InvControls rather than Generators. """ From 7de94c8ac105f4c32bad1905692a38bc1c24a290 Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 30 Sep 2025 11:19:54 +1000 Subject: [PATCH 3/5] todo changelog and test question mark Signed-off-by: vince --- changelog.md | 1 + src/zepben/eas/client/eas_client.py | 2 +- src/zepben/eas/client/work_package.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 3a01d63..9294824 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ * `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. diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 0484d3c..1808b66 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -246,7 +246,7 @@ def generator_config_to_json(self, generator_config: Optional[GeneratorConfig]) "simplifyPLSIThreshold": generator_config.model.simplify_plsi_threshold, "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.replace(microsecond=0, tzinfo=None).isoformat(), # remove any timezone information + "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 } diff --git a/src/zepben/eas/client/work_package.py b/src/zepben/eas/client/work_package.py index 9895083..3322bf0 100644 --- a/src/zepben/eas/client/work_package.py +++ b/src/zepben/eas/client/work_package.py @@ -216,11 +216,11 @@ class PVVoltVARVoltWattConfig: 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, + 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] = "default" + 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.""" From 5f0fb818b4174dd8cf2f5c7eae8a1fb7a58af9ee Mon Sep 17 00:00:00 2001 From: vince Date: Wed, 1 Oct 2025 15:10:58 +1000 Subject: [PATCH 4/5] no new tests Signed-off-by: vince --- test/test_eas_client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/test_eas_client.py b/test/test_eas_client.py index ffde47a..2982963 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -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, @@ -1169,7 +1172,8 @@ def run_opendss_export_request_handler(request): "useSpanLevelThreshold": True, "ratingThreshold": 20.0, "simplifyPLSIThreshold": 20.0, - "emergAmpScaling": 1.8 + "emergAmpScaling": 1.8, + 'inverterControlConfig': None }, "solve": { "normVMinPu": 0.9, @@ -1464,6 +1468,11 @@ def test_run_opendss_export_valid_certificate_success(ca: trustme.CA, httpserver ratingThreshold simplifyPLSIThreshold emergAmpScaling + inverterControlConfig { + cutOffDate + beforeCutOffProfile + afterCutOffProfile + } } solve { normVMinPu From 30e4ece2aeddeffa58896d7cbcf76160cf32c55c Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 2 Oct 2025 12:03:29 +1000 Subject: [PATCH 5/5] dont truncate TimePeriod to days Signed-off-by: vince --- changelog.md | 2 +- src/zepben/eas/client/work_package.py | 6 +++--- test/test_eas_client.py | 27 ++++++++++++++++++--------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index 9294824..ef6c0d8 100644 --- a/changelog.md +++ b/changelog.md @@ -19,7 +19,7 @@ * 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/work_package.py b/src/zepben/eas/client/work_package.py index 3322bf0..48051e4 100644 --- a/src/zepben/eas/client/work_package.py +++ b/src/zepben/eas/client/work_package.py @@ -148,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 @@ -166,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 diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 2982963..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 ) ) @@ -1092,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], @@ -1173,7 +1173,11 @@ def run_opendss_export_request_handler(request): "ratingThreshold": 20.0, "simplifyPLSIThreshold": 20.0, "emergAmpScaling": 1.8, - 'inverterControlConfig': None + 'inverterControlConfig': { + 'afterCutOffProfile': 'afterProfile', + 'beforeCutOffProfile': 'beforeProfile', + 'cutOffDate': '2024-04-12T11:42:00' + }, }, "solve": { "normVMinPu": 0.9, @@ -1218,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", @@ -1286,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,