Support single-zone static price in tariff_zones provider#324
Conversation
Convert the tariff_zones provider into a flexible 1-, 2-, or 3-zone mode, so it also covers the "static price mode" use case (issue #318). When only tariff_zone_1 is configured, zone_1_hours defaults to all 24 hours and the provider returns a flat price for every hour. - Make tariff_zone_2 / zone_2_hours optional (must still be paired) - Require tariff_zone_1 only in the factory; validate zone_2 pairing - Default zone_1_hours to 0-23 in single-zone mode - Update config example and docstrings - Add tests covering single-zone static price mode and zone-2 pairing
Align factory and class-level error messages for zone 2/3 paired-option checks, and clarify in the docstring that an explicitly-set zone_1_hours is not auto-extended in single-zone mode.
There was a problem hiding this comment.
Pull request overview
Adds support for using the tariff_zones dynamic tariff provider as a single-zone “static price” mode (flat price all day) while keeping existing 2-/3-zone configurations working.
Changes:
- Allow
TariffZonesto run with onlytariff_zone_1by defaultingzone_1_hoursto all 24 hours in single-zone mode. - Update
DynamicTariff.create_tarif_provider()to only requiretariff_zone_1and to validate zone-2/zone-3 price+hours pairing. - Extend docs (dummy config) and tests to cover single-zone behavior and updated error messages.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/batcontrol/dynamictariff/test_tariffzones.py | Adds/updates tests for single-zone static pricing and revised validation/error cases. |
| src/batcontrol/dynamictariff/tariffzones.py | Implements single-zone default hours and makes zone 2 optional with paired validation. |
| src/batcontrol/dynamictariff/dynamictariff.py | Loosens required fields for tariff_zones and adds zone-2/zone-3 pairing checks in the factory. |
| config/batcontrol_config_dummy.yaml | Documents the single-zone static-price configuration mode. |
| elif provider.lower() == 'tariff_zones': | ||
| required_fields = ['tariff_zone_1', 'zone_1_hours', 'tariff_zone_2', 'zone_2_hours'] | ||
| for field in required_fields: | ||
| if field not in config: | ||
| # Only tariff_zone_1 is strictly required. A single-zone | ||
| # configuration acts as a static flat price for all 24 hours. | ||
| if 'tariff_zone_1' not in config: | ||
| raise RuntimeError( | ||
| '[DynTariff] Please include tariff_zone_1 in your configuration file' | ||
| ) | ||
| # zone_2 and zone_3 are optional, but price and hours must be | ||
| # provided together for each additional zone. | ||
| for zone in (2, 3): | ||
| price_key = f'tariff_zone_{zone}' | ||
| hours_key = f'zone_{zone}_hours' | ||
| if (price_key in config) != (hours_key in config): | ||
| raise RuntimeError( | ||
| f'[DynTariff] Please include {field} in your configuration file' | ||
| f'[DynTariff] {hours_key} and {price_key} must both be ' | ||
| 'set or both omitted' | ||
| ) | ||
| zone_3_hours = config.get('zone_3_hours') | ||
| zone_1_hours = config.get('zone_1_hours') | ||
| tariff_zone_2 = config.get('tariff_zone_2') | ||
| zone_2_hours = config.get('zone_2_hours') | ||
| tariff_zone_3 = config.get('tariff_zone_3') | ||
| zone_3_hours = config.get('zone_3_hours') | ||
| selected_tariff = TariffZones( | ||
| timezone, | ||
| min_time_between_api_calls, | ||
| delay_evaluation_by_seconds, | ||
| target_resolution=target_resolution, | ||
| tariff_zone_1=float(config['tariff_zone_1']), | ||
| zone_1_hours=config['zone_1_hours'], | ||
| tariff_zone_2=float(config['tariff_zone_2']), | ||
| zone_2_hours=config['zone_2_hours'], | ||
| zone_1_hours=zone_1_hours, | ||
| tariff_zone_2=float(tariff_zone_2) if tariff_zone_2 is not None else None, | ||
| zone_2_hours=zone_2_hours, | ||
| tariff_zone_3=float(tariff_zone_3) if tariff_zone_3 is not None else None, | ||
| zone_3_hours=zone_3_hours, | ||
| ) |
There was a problem hiding this comment.
In the tariff_zones factory branch, zone_1_hours is no longer validated when additional zones are configured. This means a misconfigured 2-/3-zone setup (e.g., tariff_zone_2+zone_2_hours present but no zone_1_hours) will successfully create the provider and only fail later when prices are requested, which is a behavior regression vs. the previous required-fields check. Consider adding an explicit factory-time validation: if any additional zone (2 or 3) is configured, require zone_1_hours to be present/non-null and raise a clear [DynTariff] error immediately (and add/adjust a regression test accordingly).
There was a problem hiding this comment.
Good catch — fixed in 86c0dd0. The factory now raises immediately when tariff_zone_2 or tariff_zone_3 is present but zone_1_hours is missing, and there's a regression test (test_factory_requires_zone_1_hours_when_extra_zones_configured) covering it.
Generated by Claude Code
Addresses Copilot review feedback on #324: with zone 2 or zone 3 configured, a missing zone_1_hours used to fail at first price fetch. Restore the fail-fast check at factory construction and add a regression test.
Summary
Converts the
tariff_zonesprice mode into a flexible 1-, 2-, or 3-zone provider so it also covers the "static single price" use case from #318. When onlytariff_zone_1is configured,zone_1_hoursdefaults to all 24 hours and every hour of the day gets the same flat price — which is exactly what a non-dynamic-price user needs for peak-shaving scenarios.Existing 2-zone and 3-zone configurations are fully backwards-compatible; only the previously-required zone 2 became optional.
Changes
tariffzones.py: maketariff_zone_2/zone_2_hoursoptional (must still be paired together); defaultzone_1_hoursto0-23when zone 1 is the only configured zone; updated docstrings.dynamictariff.pyfactory: require onlytariff_zone_1; validate zone 2 / zone 3 price+hours pairing with consistent error wording.config/batcontrol_config_dummy.yaml: document the single-zone static-price mode.tests/.../test_tariffzones.py: add tests for single-zone default hours, explicit full-day hours, partial-hour rejection, factory single-zone config, missingtariff_zone_1, and orphan zone-2 price/hours; adjust pre-existing assertions to the new (clearer) error messages.Example new static-price config:
Refs #318.
Test plan
python -m pytest tests/batcontrol/dynamictariff/— 77 passed