|
30 | 30 | from .logic import CalculationInput, CalculationParameters |
31 | 31 | from .logic import CommonLogic |
32 | 32 | from .logic import PeakShavingConfig |
| 33 | +from .logic.grid_charge_target import ( |
| 34 | + GRID_CHARGE_TARGET_STRATEGIES, |
| 35 | + GRID_CHARGE_TARGET_STRATEGY_FIXED, |
| 36 | + calculate_effective_grid_charge_soc, |
| 37 | +) |
33 | 38 |
|
34 | 39 | from .dynamictariff import DynamicTariff as tariff_factory |
35 | 40 | from .inverter import Inverter as inverter_factory |
@@ -80,6 +85,27 @@ def _parse_optional_ratio(value, config_key: str) -> Optional[float]: |
80 | 85 | return ratio |
81 | 86 |
|
82 | 87 |
|
| 88 | +def _parse_ratio(value, config_key: str) -> float: |
| 89 | + """Parse a required 0..1 ratio config value.""" |
| 90 | + ratio = _parse_optional_ratio(value, config_key) |
| 91 | + if ratio is None: |
| 92 | + raise ValueError( |
| 93 | + f"{config_key} must be numeric between 0 and 1, got None" |
| 94 | + ) |
| 95 | + return ratio |
| 96 | + |
| 97 | + |
| 98 | +def _parse_grid_charge_target_strategy(value) -> str: |
| 99 | + """Parse the grid-charge target strategy config value.""" |
| 100 | + strategy = str(value).strip().lower() |
| 101 | + if strategy not in GRID_CHARGE_TARGET_STRATEGIES: |
| 102 | + raise ValueError( |
| 103 | + f"battery_control.grid_charge_target_strategy must be one of " |
| 104 | + f"{GRID_CHARGE_TARGET_STRATEGIES}, got {value!r}" |
| 105 | + ) |
| 106 | + return strategy |
| 107 | + |
| 108 | + |
83 | 109 | class Batcontrol: |
84 | 110 | """ Main class for Batcontrol, handles the logic and control of the battery system """ |
85 | 111 | general_logic = None # type: CommonLogic |
@@ -235,6 +261,16 @@ def __init__(self, configdict: dict): |
235 | 261 | self.batconfig.get('min_grid_charge_soc', None), |
236 | 262 | 'battery_control.min_grid_charge_soc' |
237 | 263 | ) |
| 264 | + self.grid_charge_target_strategy = _parse_grid_charge_target_strategy( |
| 265 | + self.batconfig.get( |
| 266 | + 'grid_charge_target_strategy', |
| 267 | + GRID_CHARGE_TARGET_STRATEGY_FIXED, |
| 268 | + ) |
| 269 | + ) |
| 270 | + self.grid_charge_forecast_pv_factor = _parse_ratio( |
| 271 | + self.batconfig.get('grid_charge_forecast_pv_factor', 1.0), |
| 272 | + 'battery_control.grid_charge_forecast_pv_factor' |
| 273 | + ) |
238 | 274 | self.preserve_min_grid_charge_soc = False |
239 | 275 | if (self.min_grid_charge_soc is not None |
240 | 276 | and self.min_grid_charge_soc > self.max_charging_from_grid_limit): |
@@ -616,12 +652,19 @@ def run(self): |
616 | 652 | self.peak_shaving_config, |
617 | 653 | enabled=peak_shaving_config_enabled and not evcc_disable_peak_shaving, |
618 | 654 | ) |
| 655 | + effective_min_grid_charge_soc = self.__calculate_effective_min_grid_charge_soc( |
| 656 | + calc_input, |
| 657 | + production, |
| 658 | + consumption, |
| 659 | + prices, |
| 660 | + ) |
| 661 | + |
619 | 662 | calc_parameters = CalculationParameters( |
620 | 663 | self.max_charging_from_grid_limit, |
621 | 664 | self.min_price_difference, |
622 | 665 | self.min_price_difference_rel, |
623 | 666 | self.get_max_capacity(), |
624 | | - min_grid_charge_soc=self.min_grid_charge_soc, |
| 667 | + min_grid_charge_soc=effective_min_grid_charge_soc, |
625 | 668 | preserve_min_grid_charge_soc=self.preserve_min_grid_charge_soc, |
626 | 669 | peak_shaving=ps_runtime, |
627 | 670 | ) |
@@ -666,6 +709,41 @@ def run(self): |
666 | 709 | else: |
667 | 710 | self.avoid_discharging() |
668 | 711 |
|
| 712 | + def __calculate_effective_min_grid_charge_soc( |
| 713 | + self, |
| 714 | + calc_input: CalculationInput, |
| 715 | + production, |
| 716 | + consumption, |
| 717 | + prices) -> Optional[float]: |
| 718 | + effective_min_grid_charge_soc = calculate_effective_grid_charge_soc( |
| 719 | + strategy=self.grid_charge_target_strategy, |
| 720 | + configured_min_grid_charge_soc=self.min_grid_charge_soc, |
| 721 | + max_charging_from_grid_limit=self.max_charging_from_grid_limit, |
| 722 | + max_capacity=self.get_max_capacity(), |
| 723 | + min_soc_energy=max( |
| 724 | + 0.0, |
| 725 | + calc_input.stored_energy - calc_input.stored_usable_energy, |
| 726 | + ), |
| 727 | + production=production, |
| 728 | + consumption=consumption, |
| 729 | + prices=prices, |
| 730 | + min_price_difference=self.min_price_difference, |
| 731 | + min_price_difference_rel=self.min_price_difference_rel, |
| 732 | + pv_forecast_factor=self.grid_charge_forecast_pv_factor, |
| 733 | + ) |
| 734 | + if effective_min_grid_charge_soc != self.min_grid_charge_soc: |
| 735 | + logger.info( |
| 736 | + 'Forecast grid-charge target raised min_grid_charge_soc ' |
| 737 | + 'from %.1f%% to %.1f%%', |
| 738 | + self.min_grid_charge_soc * 100, |
| 739 | + effective_min_grid_charge_soc * 100, |
| 740 | + ) |
| 741 | + if (self.mqtt_api is not None |
| 742 | + and effective_min_grid_charge_soc is not None): |
| 743 | + self.mqtt_api.publish_effective_min_grid_charge_soc( |
| 744 | + effective_min_grid_charge_soc) |
| 745 | + return effective_min_grid_charge_soc |
| 746 | + |
669 | 747 | def __set_charge_rate(self, charge_rate: int): |
670 | 748 | """ Set charge rate and publish to mqtt """ |
671 | 749 | self.last_charge_rate = charge_rate |
|
0 commit comments