From 8f1ebf944dae2dfbfde97303eceeb55539aa0c58 Mon Sep 17 00:00:00 2001 From: OliJue <70478960+OliJue@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:04:06 +0100 Subject: [PATCH 1/5] Resolves #136 --- config/batcontrol_config_dummy.yaml | 6 +- src/batcontrol/dynamictariff/dynamictariff.py | 26 +++++ src/batcontrol/dynamictariff/twotariffmode.py | 103 ++++++++++++++++++ tests/batcontrol/logic/test_default.py | 33 ++++++ 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/batcontrol/dynamictariff/twotariffmode.py diff --git a/config/batcontrol_config_dummy.yaml b/config/batcontrol_config_dummy.yaml index 18ec92a..3900324 100644 --- a/config/batcontrol_config_dummy.yaml +++ b/config/batcontrol_config_dummy.yaml @@ -64,10 +64,14 @@ inverter: # See more Details in: https://github.com/MaStr/batcontrol/wiki/Dynamic-tariff-provider #-------------------------- utility: - type: awattar_de # [tibber, awattar_at, awattar_de, evcc, energyforecast] + type: awattar_de # [tibber, awattar_at, awattar_de, evcc, energyforecast, twotariffmode] vat: 0.19 # only required for awattar and energyforecast fees: 0.015 # only required for awattar and energyforecast markup: 0.03 # only required for awattar and energyforecast + # tariff_day: 0.2733 # only required for twotariffmode, Euro/kWh incl. vat/fees + # tariff_night: 0.1734 # only required for twotariffmode, Euro/kWh incl. vat/fees + # day_start: 5 # only required for twotariffmode, hour of day when day tariff starts + # day_end: 0 # only required for twotariffmode, hour of day when day tariff ends # apikey: YOUR_API_KEY # MANDATORY for energyforecast and tibber. Uncomment and set if using those providers. #-------------------------- diff --git a/src/batcontrol/dynamictariff/dynamictariff.py b/src/batcontrol/dynamictariff/dynamictariff.py index b67cae1..d0c1c52 100644 --- a/src/batcontrol/dynamictariff/dynamictariff.py +++ b/src/batcontrol/dynamictariff/dynamictariff.py @@ -19,6 +19,7 @@ from .tibber import Tibber from .evcc import Evcc from .energyforecast import Energyforecast +from .twotariffmode import Twotariffmode from .dynamictariff_interface import TariffInterface @@ -129,6 +130,31 @@ def create_tarif_provider(config: dict, timezone, if provider.lower() == 'energyforecast_96': selected_tariff.upgrade_48h_to_96h() + elif provider.lower() == 'twotariffmode': + # require tariffs for day and night + required_fields = ['tariff_day', 'tariff_night'] + for field in required_fields: + if field not in config.keys(): + raise RuntimeError( + f'[DynTariff] Please include {field} in your configuration file' + ) + # read values and optional price parameters + tariff_day = float(config.get('tariff_day')) + tariff_night = float(config.get('tariff_night')) + day_start = int(config.get('day_start', 7)) + day_end = int(config.get('day_end', 22)) + selected_tariff = Twotariffmode( + timezone, + min_time_between_api_calls, + delay_evaluation_by_seconds, + target_resolution=target_resolution + ) + # store configured values in instance + selected_tariff.tariff_day = tariff_day + selected_tariff.tariff_night = tariff_night + selected_tariff.day_start = day_start + selected_tariff.day_end = day_end + else: raise RuntimeError(f'[DynamicTariff] Unkown provider {provider}') return selected_tariff diff --git a/src/batcontrol/dynamictariff/twotariffmode.py b/src/batcontrol/dynamictariff/twotariffmode.py new file mode 100644 index 0000000..8fa94ba --- /dev/null +++ b/src/batcontrol/dynamictariff/twotariffmode.py @@ -0,0 +1,103 @@ +"""TwoTariffMode provider + +Simple dynamic tariff provider that returns a repeating day/night tariff. +Config options (in utility config for provider): +- type: twotariffmode +- tariff_day: price for day hours (float) +- tariff_night: price for night hours (float) +- day_start: hour when day tariff starts (int, default 7) +- day_end: hour when day tariff ends (int, default 22) + +The class produces hourly prices (native_resolution=60) for the next 48 +hours aligned to the current hour. The baseclass will handle conversion to +15min if the target resolution is 15. + +Note: +The charge rate is not evenly distributed across the low price hours. +If you prefer a more even distribution during the low price hours, you can adjust the +soften_price_difference_on_charging to enabled +and +max_grid_charge_rate to a low value, e.g. capacity of the battery divided +by the hours of low price periods. + +If you prefer a late charging start (=optimize effiency, have battery only short +time at high SOC), you can adjust the +soften_price_difference_on_charging to disabled +""" +import datetime +import logging +from .baseclass import DynamicTariffBaseclass + +logger = logging.getLogger(__name__) + + +class Twotariffmode(DynamicTariffBaseclass): + """Two-tier tariff: day / night fixed prices.""" + + def __init__( + self, + timezone, + min_time_between_API_calls=0, + delay_evaluation_by_seconds=0, + target_resolution: int = 60, + ): + super().__init__( + timezone, + min_time_between_API_calls, + delay_evaluation_by_seconds, + target_resolution=target_resolution, + native_resolution=60, + ) + + # defaults + self.tariff_day = 0.20 + self.tariff_night = 0.10 + self.day_start = 7 + self.day_end = 22 + + def get_raw_data_from_provider(self) -> dict: + """Return the configuration-like raw data stored in cache. + + This provider is purely local and does not call external APIs. + We return a dict containing the configured values so that + `_get_prices_native` can read from `get_raw_data()` uniformly. + """ + return { + 'tariff_day': self.tariff_day, + 'tariff_night': self.tariff_night, + 'day_start': self.day_start, + 'day_end': self.day_end, + } + + def _get_prices_native(self) -> dict[int, float]: + """Build hourly prices for the next 48 hours, hour-aligned. + + Returns a dict mapping interval index (0 = start of current hour) + to price (float). + """ + raw = self.get_raw_data() + # allow values from raw data (cache) if present + tariff_day = raw.get('tariff_day', self.tariff_day) + tariff_night = raw.get('tariff_night', self.tariff_night) + day_start = int(raw.get('day_start', self.day_start)) + day_end = int(raw.get('day_end', self.day_end)) + + now = datetime.datetime.now().astimezone(self.timezone) + # Align to start of current hour + current_hour_start = now.replace(minute=0, second=0, microsecond=0) + + prices = {} + # produce next 48 hours + for rel_hour in range(0, 48): + ts = current_hour_start + datetime.timedelta(hours=rel_hour) + h = ts.hour + if day_start <= day_end: + is_day = (h >= day_start and h < day_end) + else: + # wrap-around (e.g., day_start=20, day_end=6) + is_day = not (h >= day_end and h < day_start) + + prices[rel_hour] = tariff_day if is_day else tariff_night + + logger.debug('Twotariffmode: Generated %d hourly prices', len(prices)) + return prices diff --git a/tests/batcontrol/logic/test_default.py b/tests/batcontrol/logic/test_default.py index 86f3075..fe33dac 100644 --- a/tests/batcontrol/logic/test_default.py +++ b/tests/batcontrol/logic/test_default.py @@ -188,6 +188,39 @@ def test_charge_calculation_when_charging_possible(self): self.assertGreater(result.charge_rate, 0, "Charge rate should be greater than 0") self.assertGreater(calc_output.required_recharge_energy, 0, "Should calculate required recharge energy") + def test_charge_calculation_when_charging_possible_modified(self): + """Test charge calculation when charging is possible due to low SOC""" + stored_energy = 2000 # 2 kWh, well below charging limit (79% = 7.9 kWh) + stored_usable_energy, free_capacity = self._calculate_battery_values( + stored_energy, self.max_capacity + ) + + # Setup scenario with high future prices to trigger charging + consumption = np.array([1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000]) # High future consumption that requires reserves + production = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) # No production + + calc_input = CalculationInput( + consumption=consumption, + production=production, + prices={0: 0.20, 1: 0.20, 2: .30, 3: 0.30, 4: 0.30, 5: 0.30, 6: 0.30, 7: 0.30, 8: 0.30, 9: 0.30}, # Low current price, high future prices + stored_energy=stored_energy, + stored_usable_energy=stored_usable_energy, + free_capacity=free_capacity, + ) + + # Test at 30 minutes past the hour to test charge rate calculation + # calc_timestamp = datetime.datetime(2025, 6, 20, 12, 30, 0, tzinfo=datetime.timezone.utc) + calc_timestamp = datetime.datetime(2025, 6, 20, 12, 50, 0, tzinfo=datetime.timezone.utc) + self.assertTrue(self.logic.calculate(calc_input, calc_timestamp)) + result = self.logic.get_inverter_control_settings() + calc_output = self.logic.get_calculation_output() + + # Verify charging is enabled + self.assertFalse(result.allow_discharge, "Discharge should not be allowed when charging needed") + self.assertTrue(result.charge_from_grid, "Should charge from grid when energy needed for high price hours") + self.assertGreater(result.charge_rate, 0, "Charge rate should be greater than 0") + self.assertGreater(calc_output.required_recharge_energy, 0, "Should calculate required recharge energy") + def test_charge_calculation_when_charging_not_possible_high_soc(self): """Test charge calculation when charging is not possible due to high SOC""" # Set SOC above charging limit (79%) From 6fc8952d874a7012f588ac0b06e332ace96d81c4 Mon Sep 17 00:00:00 2001 From: OliJue <70478960+OliJue@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:22:23 +0100 Subject: [PATCH 2/5] changed tariff naming --- config/batcontrol_config_dummy.yaml | 10 ++-- src/batcontrol/dynamictariff/dynamictariff.py | 26 +++++----- .../{twotariffmode.py => tariffzones.py} | 50 +++++++++---------- 3 files changed, 43 insertions(+), 43 deletions(-) rename src/batcontrol/dynamictariff/{twotariffmode.py => tariffzones.py} (66%) diff --git a/config/batcontrol_config_dummy.yaml b/config/batcontrol_config_dummy.yaml index 3900324..2cf2a09 100644 --- a/config/batcontrol_config_dummy.yaml +++ b/config/batcontrol_config_dummy.yaml @@ -64,14 +64,14 @@ inverter: # See more Details in: https://github.com/MaStr/batcontrol/wiki/Dynamic-tariff-provider #-------------------------- utility: - type: awattar_de # [tibber, awattar_at, awattar_de, evcc, energyforecast, twotariffmode] + type: awattar_de # [tibber, awattar_at, awattar_de, evcc, energyforecast, tariff_zones] vat: 0.19 # only required for awattar and energyforecast fees: 0.015 # only required for awattar and energyforecast markup: 0.03 # only required for awattar and energyforecast - # tariff_day: 0.2733 # only required for twotariffmode, Euro/kWh incl. vat/fees - # tariff_night: 0.1734 # only required for twotariffmode, Euro/kWh incl. vat/fees - # day_start: 5 # only required for twotariffmode, hour of day when day tariff starts - # day_end: 0 # only required for twotariffmode, hour of day when day tariff ends + # tariff_zone_1: 0.2733 # only required for tariff_zones, Euro/kWh incl. vat/fees + # tariff_zone_2: 0.1734 # only required for tariff_zones, Euro/kWh incl. vat/fees + # zone_1_start: 5 # only required for tariff_zones, hour of day when zone 1 tariff starts + # zone_1_end: 0 # only required for tariff_zones, hour of day when zone 1 tariff ends # apikey: YOUR_API_KEY # MANDATORY for energyforecast and tibber. Uncomment and set if using those providers. #-------------------------- diff --git a/src/batcontrol/dynamictariff/dynamictariff.py b/src/batcontrol/dynamictariff/dynamictariff.py index d0c1c52..01c5811 100644 --- a/src/batcontrol/dynamictariff/dynamictariff.py +++ b/src/batcontrol/dynamictariff/dynamictariff.py @@ -19,7 +19,7 @@ from .tibber import Tibber from .evcc import Evcc from .energyforecast import Energyforecast -from .twotariffmode import Twotariffmode +from .tariffzones import Tariff_zones from .dynamictariff_interface import TariffInterface @@ -130,30 +130,30 @@ def create_tarif_provider(config: dict, timezone, if provider.lower() == 'energyforecast_96': selected_tariff.upgrade_48h_to_96h() - elif provider.lower() == 'twotariffmode': - # require tariffs for day and night - required_fields = ['tariff_day', 'tariff_night'] + elif provider.lower() == 'tariff_zones': + # require tariffs for zone 1 and zone 2 + required_fields = ['tariff_zone_1', 'tariff_zone_2'] for field in required_fields: if field not in config.keys(): raise RuntimeError( f'[DynTariff] Please include {field} in your configuration file' ) # read values and optional price parameters - tariff_day = float(config.get('tariff_day')) - tariff_night = float(config.get('tariff_night')) - day_start = int(config.get('day_start', 7)) - day_end = int(config.get('day_end', 22)) - selected_tariff = Twotariffmode( + tariff_zone_1 = float(config.get('tariff_zone_1')) + tariff_zone_2 = float(config.get('tariff_zone_2')) + zone_1_start = int(config.get('zone_1_start', 7)) + zone_1_end = int(config.get('zone_1_end', 22)) + selected_tariff = Tariff_zones( timezone, min_time_between_api_calls, delay_evaluation_by_seconds, target_resolution=target_resolution ) # store configured values in instance - selected_tariff.tariff_day = tariff_day - selected_tariff.tariff_night = tariff_night - selected_tariff.day_start = day_start - selected_tariff.day_end = day_end + selected_tariff.tariff_zone_1 = tariff_zone_1 + selected_tariff.tariff_zone_2 = tariff_zone_2 + selected_tariff.zone_1_start = zone_1_start + selected_tariff.zone_1_end = zone_1_end else: raise RuntimeError(f'[DynamicTariff] Unkown provider {provider}') diff --git a/src/batcontrol/dynamictariff/twotariffmode.py b/src/batcontrol/dynamictariff/tariffzones.py similarity index 66% rename from src/batcontrol/dynamictariff/twotariffmode.py rename to src/batcontrol/dynamictariff/tariffzones.py index 8fa94ba..aa09bf8 100644 --- a/src/batcontrol/dynamictariff/twotariffmode.py +++ b/src/batcontrol/dynamictariff/tariffzones.py @@ -1,12 +1,12 @@ -"""TwoTariffMode provider +"""Tariff_zones provider Simple dynamic tariff provider that returns a repeating day/night tariff. Config options (in utility config for provider): -- type: twotariffmode -- tariff_day: price for day hours (float) -- tariff_night: price for night hours (float) -- day_start: hour when day tariff starts (int, default 7) -- day_end: hour when day tariff ends (int, default 22) +- type: tariff_zones +- tariff_zone_1: price for day hours (float) +- tariff_zone_2: price for night hours (float) +- zone_1_start: hour when tariff zone 1 starts (int, default 7) +- zone_1_end: hour when tariff zone 1 ends (int, default 22) The class produces hourly prices (native_resolution=60) for the next 48 hours aligned to the current hour. The baseclass will handle conversion to @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) -class Twotariffmode(DynamicTariffBaseclass): +class Tariff_zones(DynamicTariffBaseclass): """Two-tier tariff: day / night fixed prices.""" def __init__( @@ -50,10 +50,10 @@ def __init__( ) # defaults - self.tariff_day = 0.20 - self.tariff_night = 0.10 - self.day_start = 7 - self.day_end = 22 + self.tariff_zone_1 = 0.20 + self.tariff_zone_2 = 0.10 + self.zone_1_start = 7 + self.zone_1_end = 22 def get_raw_data_from_provider(self) -> dict: """Return the configuration-like raw data stored in cache. @@ -63,10 +63,10 @@ def get_raw_data_from_provider(self) -> dict: `_get_prices_native` can read from `get_raw_data()` uniformly. """ return { - 'tariff_day': self.tariff_day, - 'tariff_night': self.tariff_night, - 'day_start': self.day_start, - 'day_end': self.day_end, + 'tariff_zone_1': self.tariff_zone_1, + 'tariff_zone_2': self.tariff_zone_2, + 'zone_1_start': self.zone_1_start, + 'zone_1_end': self.zone_1_end, } def _get_prices_native(self) -> dict[int, float]: @@ -77,10 +77,10 @@ def _get_prices_native(self) -> dict[int, float]: """ raw = self.get_raw_data() # allow values from raw data (cache) if present - tariff_day = raw.get('tariff_day', self.tariff_day) - tariff_night = raw.get('tariff_night', self.tariff_night) - day_start = int(raw.get('day_start', self.day_start)) - day_end = int(raw.get('day_end', self.day_end)) + tariff_zone_1 = raw.get('tariff_zone_1', self.tariff_zone_1) + tariff_zone_2 = raw.get('tariff_zone_2', self.tariff_zone_2) + zone_1_start = int(raw.get('zone_1_start', self.zone_1_start)) + zone_1_end = int(raw.get('zone_1_end', self.zone_1_end)) now = datetime.datetime.now().astimezone(self.timezone) # Align to start of current hour @@ -91,13 +91,13 @@ def _get_prices_native(self) -> dict[int, float]: for rel_hour in range(0, 48): ts = current_hour_start + datetime.timedelta(hours=rel_hour) h = ts.hour - if day_start <= day_end: - is_day = (h >= day_start and h < day_end) + if zone_1_start <= zone_1_end: + is_day = (h >= zone_1_start and h < zone_1_end) else: - # wrap-around (e.g., day_start=20, day_end=6) - is_day = not (h >= day_end and h < day_start) + # wrap-around (e.g., zone_1_start=20, zone_1_end=6) + is_day = not (h >= zone_1_end and h < zone_1_start) - prices[rel_hour] = tariff_day if is_day else tariff_night + prices[rel_hour] = tariff_zone_1 if is_day else tariff_zone_2 - logger.debug('Twotariffmode: Generated %d hourly prices', len(prices)) + logger.debug('tariff_zones: Generated %d hourly prices', len(prices)) return prices From e0d38c2dc719178e36daa0e586cd73bf4662cdaa Mon Sep 17 00:00:00 2001 From: OliJue <70478960+OliJue@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:38:36 +0100 Subject: [PATCH 3/5] added zone based pricing --- README.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 458a7a7..73b6871 100644 --- a/README.MD +++ b/README.MD @@ -10,7 +10,7 @@ To integrate batcontrol with Home Assistant, use the following repository: [batc ## Prerequisites: 1. A PV installation with a BYD Battery and a Fronius Gen24 inverter. -2. An EPEX Spot based contract with hourly electricity pricing, like Awattar, Tibber etc. (Get a €50 bonus on sign-up to Tibber using this [link](https://invite.tibber.com/x8ci52nj).) +2. A zone based pricing, like Octopus, or an EPEX Spot based contract with hourly electricity pricing, like Awattar, Tibber etc. (Get a €50 bonus on sign-up to Tibber using this [link](https://invite.tibber.com/x8ci52nj).) 3. Customer login details to the inverter. **OR** use the MQTT inverter driver to integrate any battery/inverter system - see [MQTT Inverter Integration](#mqtt-inverter-integration) below. From ab92f2a70637a576b3dfe63c433a17dc44d43db6 Mon Sep 17 00:00:00 2001 From: Matthias Strubel Date: Fri, 27 Feb 2026 11:02:01 +0100 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- config/batcontrol_config_dummy.yaml | 2 +- src/batcontrol/dynamictariff/dynamictariff.py | 2 +- src/batcontrol/dynamictariff/tariffzones.py | 2 +- tests/batcontrol/logic/test_default.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/batcontrol_config_dummy.yaml b/config/batcontrol_config_dummy.yaml index 2cf2a09..e4e2bbd 100644 --- a/config/batcontrol_config_dummy.yaml +++ b/config/batcontrol_config_dummy.yaml @@ -69,7 +69,7 @@ utility: fees: 0.015 # only required for awattar and energyforecast markup: 0.03 # only required for awattar and energyforecast # tariff_zone_1: 0.2733 # only required for tariff_zones, Euro/kWh incl. vat/fees - # tariff_zone_2: 0.1734 # only required for tariff_zones, Euro/kWh incl. vat/fees + # tariff_zone_2: 0.1734 # only required for tariff_zones, Euro/kWh incl. vat/fees # zone_1_start: 5 # only required for tariff_zones, hour of day when zone 1 tariff starts # zone_1_end: 0 # only required for tariff_zones, hour of day when zone 1 tariff ends # apikey: YOUR_API_KEY # MANDATORY for energyforecast and tibber. Uncomment and set if using those providers. diff --git a/src/batcontrol/dynamictariff/dynamictariff.py b/src/batcontrol/dynamictariff/dynamictariff.py index 01c5811..959dc1c 100644 --- a/src/batcontrol/dynamictariff/dynamictariff.py +++ b/src/batcontrol/dynamictariff/dynamictariff.py @@ -156,5 +156,5 @@ def create_tarif_provider(config: dict, timezone, selected_tariff.zone_1_end = zone_1_end else: - raise RuntimeError(f'[DynamicTariff] Unkown provider {provider}') + raise RuntimeError(f'[DynamicTariff] Unknown provider {provider}') return selected_tariff diff --git a/src/batcontrol/dynamictariff/tariffzones.py b/src/batcontrol/dynamictariff/tariffzones.py index aa09bf8..1472d8f 100644 --- a/src/batcontrol/dynamictariff/tariffzones.py +++ b/src/batcontrol/dynamictariff/tariffzones.py @@ -20,7 +20,7 @@ max_grid_charge_rate to a low value, e.g. capacity of the battery divided by the hours of low price periods. -If you prefer a late charging start (=optimize effiency, have battery only short +If you prefer a late charging start (=optimize efficiency, have battery only short time at high SOC), you can adjust the soften_price_difference_on_charging to disabled """ diff --git a/tests/batcontrol/logic/test_default.py b/tests/batcontrol/logic/test_default.py index fe33dac..4e86ad6 100644 --- a/tests/batcontrol/logic/test_default.py +++ b/tests/batcontrol/logic/test_default.py @@ -202,14 +202,14 @@ def test_charge_calculation_when_charging_possible_modified(self): calc_input = CalculationInput( consumption=consumption, production=production, - prices={0: 0.20, 1: 0.20, 2: .30, 3: 0.30, 4: 0.30, 5: 0.30, 6: 0.30, 7: 0.30, 8: 0.30, 9: 0.30}, # Low current price, high future prices + prices={0: 0.20, 1: 0.20, 2: 0.30, 3: 0.30, 4: 0.30, 5: 0.30, 6: 0.30, 7: 0.30, 8: 0.30, 9: 0.30}, # Low current price, high future prices stored_energy=stored_energy, stored_usable_energy=stored_usable_energy, free_capacity=free_capacity, ) # Test at 30 minutes past the hour to test charge rate calculation - # calc_timestamp = datetime.datetime(2025, 6, 20, 12, 30, 0, tzinfo=datetime.timezone.utc) + calc_timestamp = datetime.datetime(2025, 6, 20, 12, 50, 0, tzinfo=datetime.timezone.utc) self.assertTrue(self.logic.calculate(calc_input, calc_timestamp)) result = self.logic.get_inverter_control_settings() From 91cbb2b631590c317c57259ce5add066ab35722b Mon Sep 17 00:00:00 2001 From: OliJue <70478960+OliJue@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:04:45 +0100 Subject: [PATCH 5/5] some adjustments according to Copilot --- config/batcontrol_config_dummy.yaml | 2 +- src/batcontrol/dynamictariff/dynamictariff.py | 6 ++-- src/batcontrol/dynamictariff/tariffzones.py | 32 ++++-------------- tests/batcontrol/logic/test_default.py | 33 ------------------- 4 files changed, 11 insertions(+), 62 deletions(-) diff --git a/config/batcontrol_config_dummy.yaml b/config/batcontrol_config_dummy.yaml index 2cf2a09..e4e2bbd 100644 --- a/config/batcontrol_config_dummy.yaml +++ b/config/batcontrol_config_dummy.yaml @@ -69,7 +69,7 @@ utility: fees: 0.015 # only required for awattar and energyforecast markup: 0.03 # only required for awattar and energyforecast # tariff_zone_1: 0.2733 # only required for tariff_zones, Euro/kWh incl. vat/fees - # tariff_zone_2: 0.1734 # only required for tariff_zones, Euro/kWh incl. vat/fees + # tariff_zone_2: 0.1734 # only required for tariff_zones, Euro/kWh incl. vat/fees # zone_1_start: 5 # only required for tariff_zones, hour of day when zone 1 tariff starts # zone_1_end: 0 # only required for tariff_zones, hour of day when zone 1 tariff ends # apikey: YOUR_API_KEY # MANDATORY for energyforecast and tibber. Uncomment and set if using those providers. diff --git a/src/batcontrol/dynamictariff/dynamictariff.py b/src/batcontrol/dynamictariff/dynamictariff.py index 01c5811..c62a8e8 100644 --- a/src/batcontrol/dynamictariff/dynamictariff.py +++ b/src/batcontrol/dynamictariff/dynamictariff.py @@ -19,7 +19,7 @@ from .tibber import Tibber from .evcc import Evcc from .energyforecast import Energyforecast -from .tariffzones import Tariff_zones +from .tariffzones import TariffZones from .dynamictariff_interface import TariffInterface @@ -143,7 +143,7 @@ def create_tarif_provider(config: dict, timezone, tariff_zone_2 = float(config.get('tariff_zone_2')) zone_1_start = int(config.get('zone_1_start', 7)) zone_1_end = int(config.get('zone_1_end', 22)) - selected_tariff = Tariff_zones( + selected_tariff = TariffZones( timezone, min_time_between_api_calls, delay_evaluation_by_seconds, @@ -156,5 +156,5 @@ def create_tarif_provider(config: dict, timezone, selected_tariff.zone_1_end = zone_1_end else: - raise RuntimeError(f'[DynamicTariff] Unkown provider {provider}') + raise RuntimeError(f'[DynamicTariff] Unknown provider {provider}') return selected_tariff diff --git a/src/batcontrol/dynamictariff/tariffzones.py b/src/batcontrol/dynamictariff/tariffzones.py index aa09bf8..70720fd 100644 --- a/src/batcontrol/dynamictariff/tariffzones.py +++ b/src/batcontrol/dynamictariff/tariffzones.py @@ -1,10 +1,10 @@ """Tariff_zones provider -Simple dynamic tariff provider that returns a repeating day/night tariff. +Simple dynamic tariff provider that returns a repeating two zone tariff. Config options (in utility config for provider): - type: tariff_zones -- tariff_zone_1: price for day hours (float) -- tariff_zone_2: price for night hours (float) +- tariff_zone_1: price for zone 1 hours (float) +- tariff_zone_2: price for zone 2 hours (float) - zone_1_start: hour when tariff zone 1 starts (int, default 7) - zone_1_end: hour when tariff zone 1 ends (int, default 22) @@ -20,7 +20,7 @@ max_grid_charge_rate to a low value, e.g. capacity of the battery divided by the hours of low price periods. -If you prefer a late charging start (=optimize effiency, have battery only short +If you prefer a late charging start (=optimize efficiency, have battery only short time at high SOC), you can adjust the soften_price_difference_on_charging to disabled """ @@ -31,8 +31,8 @@ logger = logging.getLogger(__name__) -class Tariff_zones(DynamicTariffBaseclass): - """Two-tier tariff: day / night fixed prices.""" +class TariffZones(DynamicTariffBaseclass): + """Two-tier tariff: zone 1 / zone 2 fixed prices.""" def __init__( self, @@ -49,25 +49,7 @@ def __init__( native_resolution=60, ) - # defaults - self.tariff_zone_1 = 0.20 - self.tariff_zone_2 = 0.10 - self.zone_1_start = 7 - self.zone_1_end = 22 - def get_raw_data_from_provider(self) -> dict: - """Return the configuration-like raw data stored in cache. - - This provider is purely local and does not call external APIs. - We return a dict containing the configured values so that - `_get_prices_native` can read from `get_raw_data()` uniformly. - """ - return { - 'tariff_zone_1': self.tariff_zone_1, - 'tariff_zone_2': self.tariff_zone_2, - 'zone_1_start': self.zone_1_start, - 'zone_1_end': self.zone_1_end, - } def _get_prices_native(self) -> dict[int, float]: """Build hourly prices for the next 48 hours, hour-aligned. @@ -99,5 +81,5 @@ def _get_prices_native(self) -> dict[int, float]: prices[rel_hour] = tariff_zone_1 if is_day else tariff_zone_2 - logger.debug('tariff_zones: Generated %d hourly prices', len(prices)) + logger.debug('tariffZones: Generated %d hourly prices', len(prices)) return prices diff --git a/tests/batcontrol/logic/test_default.py b/tests/batcontrol/logic/test_default.py index fe33dac..86f3075 100644 --- a/tests/batcontrol/logic/test_default.py +++ b/tests/batcontrol/logic/test_default.py @@ -188,39 +188,6 @@ def test_charge_calculation_when_charging_possible(self): self.assertGreater(result.charge_rate, 0, "Charge rate should be greater than 0") self.assertGreater(calc_output.required_recharge_energy, 0, "Should calculate required recharge energy") - def test_charge_calculation_when_charging_possible_modified(self): - """Test charge calculation when charging is possible due to low SOC""" - stored_energy = 2000 # 2 kWh, well below charging limit (79% = 7.9 kWh) - stored_usable_energy, free_capacity = self._calculate_battery_values( - stored_energy, self.max_capacity - ) - - # Setup scenario with high future prices to trigger charging - consumption = np.array([1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000]) # High future consumption that requires reserves - production = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) # No production - - calc_input = CalculationInput( - consumption=consumption, - production=production, - prices={0: 0.20, 1: 0.20, 2: .30, 3: 0.30, 4: 0.30, 5: 0.30, 6: 0.30, 7: 0.30, 8: 0.30, 9: 0.30}, # Low current price, high future prices - stored_energy=stored_energy, - stored_usable_energy=stored_usable_energy, - free_capacity=free_capacity, - ) - - # Test at 30 minutes past the hour to test charge rate calculation - # calc_timestamp = datetime.datetime(2025, 6, 20, 12, 30, 0, tzinfo=datetime.timezone.utc) - calc_timestamp = datetime.datetime(2025, 6, 20, 12, 50, 0, tzinfo=datetime.timezone.utc) - self.assertTrue(self.logic.calculate(calc_input, calc_timestamp)) - result = self.logic.get_inverter_control_settings() - calc_output = self.logic.get_calculation_output() - - # Verify charging is enabled - self.assertFalse(result.allow_discharge, "Discharge should not be allowed when charging needed") - self.assertTrue(result.charge_from_grid, "Should charge from grid when energy needed for high price hours") - self.assertGreater(result.charge_rate, 0, "Charge rate should be greater than 0") - self.assertGreater(calc_output.required_recharge_energy, 0, "Should calculate required recharge energy") - def test_charge_calculation_when_charging_not_possible_high_soc(self): """Test charge calculation when charging is not possible due to high SOC""" # Set SOC above charging limit (79%)