From f183c56087d3861496d9a86021c0a72b1b122efb Mon Sep 17 00:00:00 2001 From: Marcus Koh Date: Thu, 15 Jan 2026 14:39:10 +1100 Subject: [PATCH 1/6] first draft Signed-off-by: Marcus Koh --- src/zepben/eas/client/eas_client.py | 78 ++++++++++++++------------- src/zepben/eas/client/work_package.py | 2 +- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 1cb5034..46fa1a2 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -379,44 +379,48 @@ def work_package_config_to_json(self, work_package: Optional[WorkPackageConfig]) } } }, - "intervention": work_package.intervention and { - "baseWorkPackageId": work_package.intervention.base_work_package_id, - "yearRange": { - "maxYear": work_package.intervention.year_range.max_year, - "minYear": work_package.intervention.year_range.min_year - }, - "allocationLimitPerYear": work_package.intervention.allocation_limit_per_year, - "interventionType": work_package.intervention.intervention_type.name, - "candidateGeneration": work_package.intervention.candidate_generation and { - "type": work_package.intervention.candidate_generation.type.name, - "interventionCriteriaName": work_package.intervention.candidate_generation.intervention_criteria_name, - "voltageDeltaAvgThreshold": work_package.intervention.candidate_generation.voltage_delta_avg_threshold, - "voltageUnderLimitHoursThreshold": work_package.intervention.candidate_generation.voltage_under_limit_hours_threshold, - "voltageOverLimitHoursThreshold": work_package.intervention.candidate_generation.voltage_over_limit_hours_threshold, - "tapWeightingFactorLowerThreshold": work_package.intervention.candidate_generation.tap_weighting_factor_lower_threshold, - "tapWeightingFactorUpperThreshold": work_package.intervention.candidate_generation.tap_weighting_factor_upper_threshold, - }, - "allocationCriteria": work_package.intervention.allocation_criteria, - "specificAllocationInstance": work_package.intervention.specific_allocation_instance, - "phaseRebalanceProportions": work_package.intervention.phase_rebalance_proportions and { - "a": work_package.intervention.phase_rebalance_proportions.a, - "b": work_package.intervention.phase_rebalance_proportions.b, - "c": work_package.intervention.phase_rebalance_proportions.c - }, - "dvms": work_package.intervention.dvms and { - "lowerLimit": work_package.intervention.dvms.lower_limit, - "upperLimit": work_package.intervention.dvms.upper_limit, - "lowerPercentile": work_package.intervention.dvms.lower_percentile, - "upperPercentile": work_package.intervention.dvms.upper_percentile, - "maxIterations": work_package.intervention.dvms.max_iterations, - "regulatorConfig": { - "puTarget": work_package.intervention.dvms.regulator_config.pu_target, - "puDeadbandPercent": work_package.intervention.dvms.regulator_config.pu_deadband_percent, - "maxTapChangePerStep": work_package.intervention.dvms.regulator_config.max_tap_change_per_step, - "allowPushToLimit": work_package.intervention.dvms.regulator_config.allow_push_to_limit + "intervention": work_package.intervention and ( + { + "baseWorkPackageId": work_package.intervention.base_work_package_id, + "yearRange": { + "maxYear": work_package.intervention.year_range.max_year, + "minYear": work_package.intervention.year_range.min_year + }, + "allocationLimitPerYear": work_package.intervention.allocation_limit_per_year, + "interventionType": work_package.intervention.intervention_type.name, + "candidateGeneration": work_package.intervention.candidate_generation and { + "type": work_package.intervention.candidate_generation.type.name, + "interventionCriteriaName": work_package.intervention.candidate_generation.intervention_criteria_name, + "voltageDeltaAvgThreshold": work_package.intervention.candidate_generation.voltage_delta_avg_threshold, + "voltageUnderLimitHoursThreshold": work_package.intervention.candidate_generation.voltage_under_limit_hours_threshold, + "voltageOverLimitHoursThreshold": work_package.intervention.candidate_generation.voltage_over_limit_hours_threshold, + "tapWeightingFactorLowerThreshold": work_package.intervention.candidate_generation.tap_weighting_factor_lower_threshold, + "tapWeightingFactorUpperThreshold": work_package.intervention.candidate_generation.tap_weighting_factor_upper_threshold, + }, + "allocationCriteria": work_package.intervention.allocation_criteria, + "specificAllocationInstance": work_package.intervention.specific_allocation_instance, + "phaseRebalanceProportions": work_package.intervention.phase_rebalance_proportions and { + "a": work_package.intervention.phase_rebalance_proportions.a, + "b": work_package.intervention.phase_rebalance_proportions.b, + "c": work_package.intervention.phase_rebalance_proportions.c + }, + "dvms": work_package.intervention.dvms and { + "lowerLimit": work_package.intervention.dvms.lower_limit, + "upperLimit": work_package.intervention.dvms.upper_limit, + "lowerPercentile": work_package.intervention.dvms.lower_percentile, + "upperPercentile": work_package.intervention.dvms.upper_percentile, + "maxIterations": work_package.intervention.dvms.max_iterations, + "regulatorConfig": { + "puTarget": work_package.intervention.dvms.regulator_config.pu_target, + "puDeadbandPercent": work_package.intervention.dvms.regulator_config.pu_deadband_percent, + "maxTapChangePerStep": work_package.intervention.dvms.regulator_config.max_tap_change_per_step, + "allowPushToLimit": work_package.intervention.dvms.regulator_config.allow_push_to_limit + } } - } - } + } | { + "allocationLimitPerYear": work_package.intervention.allocation_limit_per_year + } if work_package.intervention.allocation_limit_per_year is not None else {} + ) } def run_hosting_capacity_work_package(self, work_package: WorkPackageConfig): diff --git a/src/zepben/eas/client/work_package.py b/src/zepben/eas/client/work_package.py index 7311326..b6bbe50 100644 --- a/src/zepben/eas/client/work_package.py +++ b/src/zepben/eas/client/work_package.py @@ -861,7 +861,7 @@ class InterventionConfig: All years within this range should be included in the work package. """ - allocation_limit_per_year: int + allocation_limit_per_year: Optional[int] = None """The maximum number of interventions that can be applied per year.""" intervention_type: InterventionClass From d4050d01a084b18d46be77f195b0cec6575c051c Mon Sep 17 00:00:00 2001 From: Marcus Koh Date: Fri, 16 Jan 2026 10:18:20 +1100 Subject: [PATCH 2/6] update changelog Signed-off-by: Marcus Koh --- changelog.md | 2 +- src/zepben/eas/client/eas_client.py | 1 - src/zepben/eas/client/work_package.py | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index 5c9eb5f..08fe2fc 100644 --- a/changelog.md +++ b/changelog.md @@ -7,7 +7,7 @@ * None. ### Enhancements -* None. +* `allocation_limit_per_year` is no longer a required field in `InterventionConfig`. ### Fixes * None. diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index 46fa1a2..ed150d9 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -386,7 +386,6 @@ def work_package_config_to_json(self, work_package: Optional[WorkPackageConfig]) "maxYear": work_package.intervention.year_range.max_year, "minYear": work_package.intervention.year_range.min_year }, - "allocationLimitPerYear": work_package.intervention.allocation_limit_per_year, "interventionType": work_package.intervention.intervention_type.name, "candidateGeneration": work_package.intervention.candidate_generation and { "type": work_package.intervention.candidate_generation.type.name, diff --git a/src/zepben/eas/client/work_package.py b/src/zepben/eas/client/work_package.py index b6bbe50..42e04a1 100644 --- a/src/zepben/eas/client/work_package.py +++ b/src/zepben/eas/client/work_package.py @@ -861,12 +861,12 @@ class InterventionConfig: All years within this range should be included in the work package. """ - allocation_limit_per_year: Optional[int] = None - """The maximum number of interventions that can be applied per year.""" - intervention_type: InterventionClass """The class of intervention to apply.""" + allocation_limit_per_year: Optional[int] = None + """The maximum number of interventions that can be applied per year.""" + candidate_generation: Optional[CandidateGenerationConfig] = None """ The method of generating candidates for the intervention. From 7e44a6848aca704500487f343f302cd6118d7566 Mon Sep 17 00:00:00 2001 From: Marcus Koh Date: Fri, 16 Jan 2026 10:44:55 +1100 Subject: [PATCH 3/6] add tests Signed-off-by: Marcus Koh --- src/zepben/eas/client/eas_client.py | 6 +-- test/test_eas_client.py | 71 ++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index ed150d9..b13b7d9 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -282,7 +282,7 @@ def generator_config_to_json(self, generator_config: Optional[GeneratorConfig]) } } - def work_package_config_to_json(self, work_package: Optional[WorkPackageConfig]) -> Optional[dict]: + def work_package_config_to_json(self, work_package: WorkPackageConfig) -> dict: return { "feederConfigs": { "configs": [ @@ -416,9 +416,9 @@ def work_package_config_to_json(self, work_package: Optional[WorkPackageConfig]) "allowPushToLimit": work_package.intervention.dvms.regulator_config.allow_push_to_limit } } - } | { + } | ({ "allocationLimitPerYear": work_package.intervention.allocation_limit_per_year - } if work_package.intervention.allocation_limit_per_year is not None else {} + } if work_package.intervention.allocation_limit_per_year is not None else {}) ) } diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 83bc6f8..758649b 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -17,7 +17,7 @@ from werkzeug import Response from zepben.ewb.auth import ZepbenTokenFetcher -from zepben.eas import EasClient, Study, SolveConfig +from zepben.eas import EasClient, Study, SolveConfig, InterventionConfig, YearRange from zepben.eas import FeederConfig, ForecastConfig, FixedTimeLoadOverride from zepben.eas.client.ingestor import IngestorConfigInput, IngestorRunsSortCriteriaInput, IngestorRunsFilterInput, \ IngestorRunState, IngestorRuntimeKind @@ -26,7 +26,7 @@ Order from zepben.eas.client.study import Result from zepben.eas.client.work_package import FeederConfigs, TimePeriodLoadOverride, \ - FixedTime, NodeLevelResultsConfig, PVVoltVARVoltWattConfig + FixedTime, NodeLevelResultsConfig, PVVoltVARVoltWattConfig, InterventionClass from zepben.eas.client.work_package import WorkPackageConfig, TimePeriod, GeneratorConfig, ModelConfig, \ FeederScenarioAllocationStrategy, LoadPlacement, MeterPlacementConfig, SwitchMeterPlacementConfig, SwitchClass, \ SolveMode, RawResultsConfig @@ -1682,3 +1682,70 @@ def test_get_ingestor_run_list_all_filters_no_verify_success(httpserver: HTTPSer ) httpserver.check_assertions() assert res == {"result": "success"} + + +def test_work_package_config_to_json_omits_allocation_limit_per_year_if_unspecified(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + wp_config = WorkPackageConfig( + name="wp", + syf_config=FeederConfigs([]), + intervention=InterventionConfig( + base_work_package_id="abc", + year_range=YearRange(2020, 2025), + intervention_type=InterventionClass.COMMUNITY_BESS + ) + ) + json_config = eas_client.work_package_config_to_json(wp_config) + + assert json_config["intervention"] == { + "baseWorkPackageId": "abc", + "yearRange": { + "maxYear": 2025, + "minYear": 2020 + }, + "interventionType": "COMMUNITY_BESS", + "candidateGeneration": None, + "allocationCriteria": None, + "specificAllocationInstance": None, + "phaseRebalanceProportions": None, + "dvms": None + } + +def test_work_package_config_to_json_includes_allocation_limit_per_year_if_specified(httpserver: HTTPServer): + eas_client = EasClient( + LOCALHOST, + httpserver.port, + verify_certificate=False + ) + + wp_config = WorkPackageConfig( + name="wp", + syf_config=FeederConfigs([]), + intervention=InterventionConfig( + base_work_package_id="abc", + year_range=YearRange(2020, 2025), + intervention_type=InterventionClass.COMMUNITY_BESS, + allocation_limit_per_year=5 + ) + ) + json_config = eas_client.work_package_config_to_json(wp_config) + + assert json_config["intervention"] == { + "baseWorkPackageId": "abc", + "yearRange": { + "maxYear": 2025, + "minYear": 2020 + }, + "interventionType": "COMMUNITY_BESS", + "candidateGeneration": None, + "allocationCriteria": None, + "specificAllocationInstance": None, + "phaseRebalanceProportions": None, + "dvms": None, + "allocationLimitPerYear": 5 + } From e711d363f350fa2e26e288150fe7821bb0a760fd Mon Sep 17 00:00:00 2001 From: Marcus Koh Date: Fri, 16 Jan 2026 10:46:51 +1100 Subject: [PATCH 4/6] update breaking changes Signed-off-by: Marcus Koh --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 08fe2fc..4718293 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ## [0.27.0] - UNRELEASED ### Breaking Changes * Bumping `urllib3` to `v2.5.0`, and pulling in `zepben.auth` via the SDK. +* EAS must support unspecified `allocationLimitPerYear` in the intervention config. ### New Features * None. From 031ecad704c811d9f57ab70e88aa6a9de201bdcd Mon Sep 17 00:00:00 2001 From: Marcus Koh Date: Fri, 16 Jan 2026 14:02:28 +1100 Subject: [PATCH 5/6] allow unspecified year range Signed-off-by: Marcus Koh --- src/zepben/eas/client/eas_client.py | 21 ++++++++++++++------- src/zepben/eas/client/work_package.py | 8 ++++---- test/test_eas_client.py | 9 ++------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/zepben/eas/client/eas_client.py b/src/zepben/eas/client/eas_client.py index b13b7d9..ec9f292 100644 --- a/src/zepben/eas/client/eas_client.py +++ b/src/zepben/eas/client/eas_client.py @@ -382,10 +382,6 @@ def work_package_config_to_json(self, work_package: WorkPackageConfig) -> dict: "intervention": work_package.intervention and ( { "baseWorkPackageId": work_package.intervention.base_work_package_id, - "yearRange": { - "maxYear": work_package.intervention.year_range.max_year, - "minYear": work_package.intervention.year_range.min_year - }, "interventionType": work_package.intervention.intervention_type.name, "candidateGeneration": work_package.intervention.candidate_generation and { "type": work_package.intervention.candidate_generation.type.name, @@ -416,9 +412,20 @@ def work_package_config_to_json(self, work_package: WorkPackageConfig) -> dict: "allowPushToLimit": work_package.intervention.dvms.regulator_config.allow_push_to_limit } } - } | ({ - "allocationLimitPerYear": work_package.intervention.allocation_limit_per_year - } if work_package.intervention.allocation_limit_per_year is not None else {}) + } | + ( + { "allocationLimitPerYear": work_package.intervention.allocation_limit_per_year } + if work_package.intervention.allocation_limit_per_year is not None else {} + ) | + ( + { + "yearRange": { + "maxYear": work_package.intervention.year_range.max_year, + "minYear": work_package.intervention.year_range.min_year + } + } + if work_package.intervention.year_range is not None else {} + ) ) } diff --git a/src/zepben/eas/client/work_package.py b/src/zepben/eas/client/work_package.py index 42e04a1..caa60e9 100644 --- a/src/zepben/eas/client/work_package.py +++ b/src/zepben/eas/client/work_package.py @@ -855,15 +855,15 @@ class InterventionConfig: The new work package should process a subset of its feeders, scenarios, and years. """ - year_range: YearRange + intervention_type: InterventionClass + """The class of intervention to apply.""" + + year_range: Optional[YearRange] = None """ The range of years to search for and apply interventions. All years within this range should be included in the work package. """ - intervention_type: InterventionClass - """The class of intervention to apply.""" - allocation_limit_per_year: Optional[int] = None """The maximum number of interventions that can be applied per year.""" diff --git a/test/test_eas_client.py b/test/test_eas_client.py index 758649b..e8b68a3 100644 --- a/test/test_eas_client.py +++ b/test/test_eas_client.py @@ -1684,7 +1684,7 @@ def test_get_ingestor_run_list_all_filters_no_verify_success(httpserver: HTTPSer assert res == {"result": "success"} -def test_work_package_config_to_json_omits_allocation_limit_per_year_if_unspecified(httpserver: HTTPServer): +def test_work_package_config_to_json_omits_server_defaulted_fields_if_unspecified(httpserver: HTTPServer): eas_client = EasClient( LOCALHOST, httpserver.port, @@ -1696,7 +1696,6 @@ def test_work_package_config_to_json_omits_allocation_limit_per_year_if_unspecif syf_config=FeederConfigs([]), intervention=InterventionConfig( base_work_package_id="abc", - year_range=YearRange(2020, 2025), intervention_type=InterventionClass.COMMUNITY_BESS ) ) @@ -1704,10 +1703,6 @@ def test_work_package_config_to_json_omits_allocation_limit_per_year_if_unspecif assert json_config["intervention"] == { "baseWorkPackageId": "abc", - "yearRange": { - "maxYear": 2025, - "minYear": 2020 - }, "interventionType": "COMMUNITY_BESS", "candidateGeneration": None, "allocationCriteria": None, @@ -1716,7 +1711,7 @@ def test_work_package_config_to_json_omits_allocation_limit_per_year_if_unspecif "dvms": None } -def test_work_package_config_to_json_includes_allocation_limit_per_year_if_specified(httpserver: HTTPServer): +def test_work_package_config_to_json_includes_server_defaulted_fields_if_specified(httpserver: HTTPServer): eas_client = EasClient( LOCALHOST, httpserver.port, From a7f3e7b8caeba40e88b5e9f6e56fcc963b119cc2 Mon Sep 17 00:00:00 2001 From: Marcus Koh Date: Fri, 16 Jan 2026 14:04:49 +1100 Subject: [PATCH 6/6] make year_range default to null Signed-off-by: Marcus Koh --- changelog.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 4718293..3cc85b8 100644 --- a/changelog.md +++ b/changelog.md @@ -2,13 +2,16 @@ ## [0.27.0] - UNRELEASED ### Breaking Changes * Bumping `urllib3` to `v2.5.0`, and pulling in `zepben.auth` via the SDK. -* EAS must support unspecified `allocationLimitPerYear` in the intervention config. +* EAS must support unspecified `allocationLimitPerYear` and `yearRange` in the intervention config. ### New Features * None. ### Enhancements -* `allocation_limit_per_year` is no longer a required field in `InterventionConfig`. +* To reduce confusion when running certain classes of intervention, the following fields are no longe required in `InterventionConfig`, + and are defaulted to sensible values server-side: + * `yearRange` + * `allocation_limit_per_year` ### Fixes * None.