diff --git a/openapi.json b/openapi.json index 6cf3fb12..362f12ee 100644 --- a/openapi.json +++ b/openapi.json @@ -11249,6 +11249,19 @@ "GENETIC0" ] }, + "ac_charge_limits_total_charge": { + "type": "boolean", + "title": "Ac Charge Limits Total Charge", + "description": "True if the AC charge setpoint caps the battery's total charge power, PV included. Some hybrid inverters (e.g. Deye in time-of-use grid charging) limit the whole charge current to the grid charge current; PV surplus above it is exported, not stored. False keeps the default model: PV surplus charges first and the grid adds ac_charge x max_charge_power_w on top.", + "default": false, + "examples": [ + false, + true + ], + "x-scope": [ + "GENETIC" + ] + }, "battery_id": { "anyOf": [ { @@ -11647,6 +11660,19 @@ "GENETIC0" ] }, + "ac_charge_limits_total_charge": { + "type": "boolean", + "title": "Ac Charge Limits Total Charge", + "description": "True if the AC charge setpoint caps the battery's total charge power, PV included. Some hybrid inverters (e.g. Deye in time-of-use grid charging) limit the whole charge current to the grid charge current; PV surplus above it is exported, not stored. False keeps the default model: PV surplus charges first and the grid adds ac_charge x max_charge_power_w on top.", + "default": false, + "examples": [ + false, + true + ], + "x-scope": [ + "GENETIC" + ] + }, "battery_id": { "anyOf": [ { @@ -12061,6 +12087,16 @@ 0, 5000 ] + }, + "ac_charge_limits_total_charge": { + "type": "boolean", + "title": "Ac Charge Limits Total Charge", + "description": "True if the AC charge setpoint caps the battery's total charge power, PV included. PV surplus above it is exported, not stored. False keeps the default model: PV surplus charges first and the grid adds ac_charge x max_charge_power_w on top.", + "default": false, + "examples": [ + false, + true + ] } }, "additionalProperties": false, diff --git a/src/akkudoktoreos/devices/genetic/battery.py b/src/akkudoktoreos/devices/genetic/battery.py index f6db6b12..2bef2450 100644 --- a/src/akkudoktoreos/devices/genetic/battery.py +++ b/src/akkudoktoreos/devices/genetic/battery.py @@ -213,6 +213,9 @@ class Battery: self.charge_array = np.full(self.prediction_hours, 0) self._discharged_raw_wh_per_slot = np.zeros(self.prediction_hours, dtype=float) self._charged_raw_wh_per_slot = np.zeros(self.prediction_hours, dtype=float) + # Optional per-slot cap on the raw charge energy from all sources. It is + # unbounded unless an inverter restricts a slot (see limit_slot_charge). + self._charge_limit_raw_wh_per_slot = np.full(self.prediction_hours, np.inf) self.soc_wh = (self.initial_soc_percentage / 100) * self.capacity_wh self.min_soc_wh = (self.min_soc_percentage / 100) * self.capacity_wh self.max_soc_wh = (self.max_soc_percentage / 100) * self.capacity_wh @@ -258,6 +261,21 @@ class Battery: self.charge_array = np.full(self.prediction_hours, 0) self._discharged_raw_wh_per_slot = np.zeros(self.prediction_hours, dtype=float) self._charged_raw_wh_per_slot = np.zeros(self.prediction_hours, dtype=float) + self._charge_limit_raw_wh_per_slot = np.full(self.prediction_hours, np.inf) + + def limit_slot_charge(self, hour: int, raw_wh: float) -> None: + """Cap the raw energy the battery may take in one slot, from all sources. + + The cap covers PV and grid charging together and is not raised by later + calls within the same slot. It is lifted again by ``reset()``. + + Args: + hour (int): Slot index. + raw_wh (float): Maximum raw charge energy [Wh] before charging efficiency. + """ + self._charge_limit_raw_wh_per_slot[hour] = min( + self._charge_limit_raw_wh_per_slot[hour], max(float(raw_wh), 0.0) + ) def rated_discharge_energy_wh(self) -> float: """Return the DC energy one full-power discharge slot delivers. @@ -388,6 +406,7 @@ class Battery: - Available SoC headroom (``max_soc_wh − soc_wh``) - ``max_charge_power_w`` + - A slot limit set by ``limit_slot_charge()``, if any - ``charging_efficiency`` Args: @@ -428,7 +447,11 @@ class Battery: # Scale the power cap [W] to a per-slot energy cap [Wh] (W x slot hours). # At slot_duration_h=1.0 (hourly) this equals the legacy power value. max_charge_per_slot_wh_fast = max( - self.max_charge_power_w * self.slot_duration_h - self._charged_raw_wh_per_slot[hour], + min( + self.max_charge_power_w * self.slot_duration_h, + self._charge_limit_raw_wh_per_slot[hour], + ) + - self._charged_raw_wh_per_slot[hour], 0.0, ) charging_efficiency_fast = self.charging_efficiency diff --git a/src/akkudoktoreos/devices/genetic/inverter.py b/src/akkudoktoreos/devices/genetic/inverter.py index ea71c9c5..9458d382 100644 --- a/src/akkudoktoreos/devices/genetic/inverter.py +++ b/src/akkudoktoreos/devices/genetic/inverter.py @@ -56,6 +56,18 @@ class InverterParameters(DeviceParameters): "examples": [None, 0, 5000], }, ) + ac_charge_limits_total_charge: bool = Field( + default=False, + json_schema_extra={ + "description": ( + "True if the AC charge setpoint caps the battery's total charge power, " + "PV included. PV surplus above it is exported, not stored. False keeps " + "the default model: PV surplus charges first and the grid adds " + "ac_charge x max_charge_power_w on top." + ), + "examples": [False, True], + }, + ) class Inverter: @@ -84,6 +96,65 @@ class Inverter: # This value remains a power [W]. GeneticSimulation converts it into a # slot-independent charge-factor limit. self.max_ac_charge_power_w = self.parameters.max_ac_charge_power_w + self.ac_charge_limits_total_charge = self.parameters.ac_charge_limits_total_charge + + def ac_charge_factor(self, factor: float) -> float: + """Return the AC charge factor the inverter can actually execute. + + The factor is a fraction of the battery's ``max_charge_power_w``. It is + capped so that the AC input stays within ``max_ac_charge_power_w`` and + is 0.0 when AC charging is impossible. + """ + if factor <= 0.0 or not self.battery or self.ac_to_dc_efficiency <= 0.0: + return 0.0 + if self.max_ac_charge_power_w is not None and self.battery.max_charge_power_w > 0: + # DC power = max_charge_power_w * factor + # AC power = DC power / ac_to_dc_eff <= max_ac_charge_power_w + factor = min( + factor, + self.max_ac_charge_power_w + * self.ac_to_dc_efficiency + / self.battery.max_charge_power_w, + ) + return max(factor, 0.0) + + def begin_ac_charge_slot(self, hour: int, factor: float) -> None: + """Apply the AC charge setpoint of a slot before its PV is processed. + + On inverters whose grid charge setpoint caps the total charge power, the + battery takes at most ``factor`` of its rated charge power in this slot, + PV included. Call ``process_energy`` afterwards so PV surplus above the + cap is exported, as the inverter does. + """ + if self.ac_charge_limits_total_charge and self.battery and factor > 0.0: + self.battery.limit_slot_charge( + hour, self.battery.max_charge_power_w * self.slot_duration_h * factor + ) + + def charge_battery_from_grid(self, hour: int, factor: float) -> tuple[float, float]: + """Charge the battery from the grid after PV was processed in this slot. + + Default model: the grid adds ``factor`` of the rated charge power on top + of the PV charge. With ``ac_charge_limits_total_charge`` the grid only + fills what PV left of the slot cap set by ``begin_ac_charge_slot``. + + Returns: + tuple[float, float]: AC energy drawn from the grid [Wh] and the + battery plus AC-to-DC conversion losses [Wh]. + """ + if not self.battery or factor <= 0.0: + return 0.0, 0.0 + if self.ac_charge_limits_total_charge: + stored, battery_losses = self.battery.charge_energy( + self.battery.max_charge_power_w * self.slot_duration_h * factor, hour + ) + else: + stored, battery_losses = self.battery.charge_energy(None, hour, charge_factor=factor) + # DC energy entering the battery (before battery internal efficiency) + dc_energy = stored + battery_losses + # AC energy consumed from grid (accounts for AC->DC conversion loss) + ac_energy = dc_energy / self.ac_to_dc_efficiency + return ac_energy, battery_losses + (ac_energy - dc_energy) def _discharge_battery_to_ac(self, requested_ac_wh: float, hour: int) -> tuple[float, float]: """Discharge battery energy and convert it to AC energy.""" diff --git a/src/akkudoktoreos/devices/settings/invertersettings.py b/src/akkudoktoreos/devices/settings/invertersettings.py index b142b641..d3221ad0 100644 --- a/src/akkudoktoreos/devices/settings/invertersettings.py +++ b/src/akkudoktoreos/devices/settings/invertersettings.py @@ -74,6 +74,21 @@ class InverterCommonSettings(DevicesBaseSettings): "x-scope": [str(ConfigScope.GENETIC), str(ConfigScope.GENETIC0)], }, ) + ac_charge_limits_total_charge: bool = Field( + default=False, + json_schema_extra={ + "description": ( + "True if the AC charge setpoint caps the battery's total charge power, " + "PV included. Some hybrid inverters (e.g. Deye in time-of-use grid " + "charging) limit the whole charge current to the grid charge current; " + "PV surplus above it is exported, not stored. False keeps the default " + "model: PV surplus charges first and the grid adds " + "ac_charge x max_charge_power_w on top." + ), + "examples": [False, True], + "x-scope": [str(ConfigScope.GENETIC)], + }, + ) battery_id: Optional[str] = Field( default=None, json_schema_extra={ @@ -361,6 +376,7 @@ class InverterCommonSettings(DevicesBaseSettings): ac_to_dc_efficiency=self.ac_to_dc_efficiency, dc_to_ac_efficiency=self.dc_to_ac_efficiency, max_ac_charge_power_w=self.max_ac_charge_power_w, + ac_charge_limits_total_charge=self.ac_charge_limits_total_charge, ) # ------------------------------------------------------------------ diff --git a/src/akkudoktoreos/optimization/genetic/genetic.py b/src/akkudoktoreos/optimization/genetic/genetic.py index 398f0c3c..edd02f70 100644 --- a/src/akkudoktoreos/optimization/genetic/genetic.py +++ b/src/akkudoktoreos/optimization/genetic/genetic.py @@ -494,7 +494,18 @@ class GeneticSimulation(PydanticBaseModel): 0.0 ) + # AC charge factor of this slot, capped by max_ac_charge_power_w + ac_charge_factor = 0.0 + if battery_fast and ac_charging_possible: + ac_charge_factor = ac_charge_hours_fast[hour] + if inverter_fast: + ac_charge_factor = inverter_fast.ac_charge_factor(ac_charge_factor) + if inverter_fast: + # Some inverters cap the total charge power with the AC setpoint; + # that cap has to be in place before PV charges the battery. + if ac_charge_factor > 0.0: + inverter_fast.begin_ac_charge_slot(hour, ac_charge_factor) energy_produced = pv_prediction_wh_fast[hour] hourly_feed_in_tariff = elect_revenue_per_hour_arr_fast[hour] # bat_grid_export_hours carries the export level per slot: @@ -522,39 +533,21 @@ class GeneticSimulation(PydanticBaseModel): hourly_feed_in_tariff = elect_revenue_per_hour_arr_fast[hour] # AC PV Battery Charge - if battery_fast: - hour_ac_charge = ac_charge_hours_fast[hour] - if hour_ac_charge > 0.0 and ac_charging_possible: - # Cap charge factor by max_ac_charge_power_w if set - effective_charge_factor = hour_ac_charge - if max_ac_charge_w_fast is not None and battery_fast.max_charge_power_w > 0: - # DC power = max_charge_power_w * factor - # AC power = DC power / ac_to_dc_eff - # AC power must be <= max_ac_charge_power_w - max_dc_factor = ( - max_ac_charge_w_fast * ac_to_dc_eff_fast - ) / battery_fast.max_charge_power_w - effective_charge_factor = min(effective_charge_factor, max_dc_factor) - - if effective_charge_factor > 0: - battery_charged_energy_actual, battery_losses_actual = ( - battery_fast.charge_energy( - None, hour, charge_factor=effective_charge_factor - ) - ) - - # DC energy entering the battery (before battery internal efficiency) - dc_energy = battery_charged_energy_actual + battery_losses_actual - # AC energy consumed from grid (accounts for AC→DC conversion loss) - ac_energy = dc_energy / ac_to_dc_eff_fast - # Inverter AC→DC conversion losses - inverter_charge_losses = ac_energy - dc_energy - - consumption += ac_energy - energy_consumption_grid_actual += ac_energy - losses_wh_per_hour[hour_idx] += ( - battery_losses_actual + inverter_charge_losses - ) + if ac_charge_factor > 0.0 and battery_fast: + if inverter_fast: + ac_energy, ac_charge_losses = inverter_fast.charge_battery_from_grid( + hour, ac_charge_factor + ) + else: + # Without an inverter the grid charges the battery losslessly + # (AC-to-DC efficiency 1.0). + stored, ac_charge_losses = battery_fast.charge_energy( + None, hour, charge_factor=ac_charge_factor + ) + ac_energy = stored + ac_charge_losses + consumption += ac_energy + energy_consumption_grid_actual += ac_energy + losses_wh_per_hour[hour_idx] += ac_charge_losses # Update hourly arrays if ( diff --git a/src/akkudoktoreos/optimization/genetic/tailvalue.py b/src/akkudoktoreos/optimization/genetic/tailvalue.py index c28c98b6..1a952410 100644 --- a/src/akkudoktoreos/optimization/genetic/tailvalue.py +++ b/src/akkudoktoreos/optimization/genetic/tailvalue.py @@ -50,11 +50,15 @@ def _simulate_action( bat.soc_wh = float(energy_wh) bat._charged_raw_wh_per_slot.fill(0) bat._discharged_raw_wh_per_slot.fill(0) + bat._charge_limit_raw_wh_per_slot.fill(np.inf) ac_enabled = inv.ac_to_dc_efficiency > 0 and ( inv.max_ac_charge_power_w is None or inv.max_ac_charge_power_w > 0 ) bat.charge_array[0] = ac_rate if ac_rate > 0 and ac_enabled else dc bat.discharge_array[0] = discharge if export == 0 or tariff > 0 else 0 + rate = inv.ac_charge_factor(ac_rate) + if rate > 0: + inv.begin_ac_charge_slot(0, rate) sold, bought, losses, _ = inv.process_energy( pv, load, @@ -64,18 +68,11 @@ def _simulate_action( ) ac_grid_charge_wh = 0.0 if ac_rate > 0 and inv.ac_to_dc_efficiency > 0: - rate = ac_rate - if inv.max_ac_charge_power_w is not None and bat.max_charge_power_w > 0: - rate = min( - rate, - inv.max_ac_charge_power_w * inv.ac_to_dc_efficiency / bat.max_charge_power_w, - ) bat.charge_array[0] = rate if rate > 0: - stored, loss = bat.charge_energy(None, 0, charge_factor=rate) - ac_grid_charge_wh = (stored + loss) / inv.ac_to_dc_efficiency + ac_grid_charge_wh, ac_losses = inv.charge_battery_from_grid(0, rate) bought += ac_grid_charge_wh - losses += loss + max(ac_grid_charge_wh - stored - loss, 0.0) + losses += ac_losses if direct_marketing and tariff < 0: sold = 0.0 discharged_wh = bat.discharged_energy_wh(0) diff --git a/tests/test_ac_charge_total_limit.py b/tests/test_ac_charge_total_limit.py new file mode 100644 index 00000000..fa186c8d --- /dev/null +++ b/tests/test_ac_charge_total_limit.py @@ -0,0 +1,244 @@ +"""Tests for inverters whose AC charge setpoint caps the total battery charge. + +With ``ac_charge_limits_total_charge`` an AC slot charges the battery with at +most ``ac_charge x max_charge_power_w``, PV included (e.g. Deye time-of-use grid +charging). PV surplus above that cap is exported; the grid only fills what PV +leaves of the cap. Without the flag the default model applies: PV charges first +and the grid adds ``ac_charge`` of the remaining charge power on top. +""" + +from unittest.mock import Mock, patch + +import numpy as np +import pytest + +from akkudoktoreos.devices.genetic.battery import Battery, SolarPanelBatteryParameters +from akkudoktoreos.devices.genetic.inverter import Inverter, InverterParameters +from akkudoktoreos.devices.settings.invertersettings import InverterCommonSettings + +MAX_CHARGE_W = 5000 + + +def _build(limits_total: bool, max_ac_charge_power_w=None) -> tuple[Inverter, Battery]: + battery = Battery( + SolarPanelBatteryParameters( + device_id="battery1", + capacity_wh=20000, + initial_soc_percentage=20, + charging_efficiency=0.9, + discharging_efficiency=0.9, + min_soc_percentage=0, + max_soc_percentage=100, + max_charge_power_w=MAX_CHARGE_W, + ), + prediction_hours=4, + ) + battery.reset() + # reset() creates an integer array; charge factors are fractions. + battery.charge_array = np.zeros(4) + predictor = Mock() + predictor.calculate_expected_direct_consumption.side_effect = min + with patch( + "akkudoktoreos.devices.genetic.inverter.get_eos_load_interpolator", + return_value=predictor, + ): + inverter = Inverter( + InverterParameters( + device_id="inverter1", + max_power_wh=10000, + battery_id="battery1", + ac_to_dc_efficiency=1.0, + max_ac_charge_power_w=max_ac_charge_power_w, + ac_charge_limits_total_charge=limits_total, + ), + battery=battery, + ) + return inverter, battery + + +def _run_ac_slot(inverter: Inverter, battery: Battery, pv_wh: float, factor: float): + """Run one AC slot the way GeneticSimulation.simulate() does.""" + battery.charge_array[0] = factor + battery.discharge_array[0] = 0 + rate = inverter.ac_charge_factor(factor) + inverter.begin_ac_charge_slot(0, rate) + export, grid_import, _, _ = inverter.process_energy(pv_wh, 0.0, 0) + ac_wh, _ = inverter.charge_battery_from_grid(0, rate) + return export, grid_import + ac_wh, battery._charged_raw_wh_per_slot[0] + + +class TestPvSurplusAboveCap: + """PV alone exceeds the AC setpoint - the Deye situation that exported PV.""" + + def test_total_limit_exports_pv_above_the_cap(self): + inverter, battery = _build(limits_total=True) + export, grid, charged_raw = _run_ac_slot(inverter, battery, pv_wh=6000, factor=0.5) + + assert charged_raw == pytest.approx(0.5 * MAX_CHARGE_W) + assert export == pytest.approx(6000 - 0.5 * MAX_CHARGE_W) + assert grid == pytest.approx(0.0) + + def test_default_model_stores_pv_up_to_full_charge_power(self): + inverter, battery = _build(limits_total=False) + export, grid, charged_raw = _run_ac_slot(inverter, battery, pv_wh=6000, factor=0.5) + + assert charged_raw == pytest.approx(MAX_CHARGE_W) + assert export == pytest.approx(6000 - MAX_CHARGE_W) + assert grid == pytest.approx(0.0) + + +class TestPvSurplusBelowCap: + """PV does not reach the setpoint - the grid tops up.""" + + def test_total_limit_grid_fills_up_to_the_cap(self): + inverter, battery = _build(limits_total=True) + export, grid, charged_raw = _run_ac_slot(inverter, battery, pv_wh=1000, factor=0.5) + + assert charged_raw == pytest.approx(0.5 * MAX_CHARGE_W) + assert grid == pytest.approx(0.5 * MAX_CHARGE_W - 1000) + assert export == pytest.approx(0.0) + + def test_default_model_grid_adds_factor_of_remaining_power(self): + inverter, battery = _build(limits_total=False) + export, grid, charged_raw = _run_ac_slot(inverter, battery, pv_wh=1000, factor=0.5) + + assert grid == pytest.approx(0.5 * (MAX_CHARGE_W - 1000)) + assert charged_raw == pytest.approx(1000 + 0.5 * (MAX_CHARGE_W - 1000)) + assert export == pytest.approx(0.0) + + +def test_max_ac_charge_power_caps_the_total_charge(): + """max_ac_charge_power_w lowers the setpoint and with it the total cap.""" + inverter, battery = _build(limits_total=True, max_ac_charge_power_w=2000) + export, grid, charged_raw = _run_ac_slot(inverter, battery, pv_wh=6000, factor=1.0) + + assert charged_raw == pytest.approx(2000) + assert export == pytest.approx(4000) + assert grid == pytest.approx(0.0) + + +def test_dc_slot_is_not_capped(): + """Only AC slots are capped: a DC slot takes PV up to max_charge_power_w.""" + inverter, battery = _build(limits_total=True) + battery.charge_array[0] = 1 + battery.discharge_array[0] = 0 + export, _, _, _ = inverter.process_energy(6000, 0.0, 0) + + assert battery._charged_raw_wh_per_slot[0] == pytest.approx(MAX_CHARGE_W) + assert export == pytest.approx(1000) + + +def test_reset_lifts_the_slot_cap(): + inverter, battery = _build(limits_total=True) + _run_ac_slot(inverter, battery, pv_wh=6000, factor=0.5) + battery.reset() + battery.charge_array = np.zeros(4) + battery.charge_array[0] = 1 + inverter.process_energy(6000, 0.0, 0) + + assert battery._charged_raw_wh_per_slot[0] == pytest.approx(MAX_CHARGE_W) + + +class TestGeneticSimulation: + """The flag changes the simulated plan, so the optimizer can prefer DC.""" + + @pytest.fixture + def simulate(self, config_eos): + from akkudoktoreos.optimization.genetic.genetic import GeneticSimulation + from akkudoktoreos.optimization.genetic.geneticparams import ( + GeneticEnergyManagementParameters, + ) + + config_eos.merge_settings_from_dict( + {"prediction": {"hours": 48}, "optimization": {"hours": 24}} + ) + hours = config_eos.prediction.hours + + def _simulate(limits_total: bool, ac_factor: float, dc_factor: float): + battery = Battery( + SolarPanelBatteryParameters( + device_id="battery1", + capacity_wh=30000, + initial_soc_percentage=20, + charging_efficiency=0.9, + discharging_efficiency=0.9, + min_soc_percentage=0, + max_soc_percentage=100, + max_charge_power_w=MAX_CHARGE_W, + ), + prediction_hours=hours, + ) + battery.reset() + inverter = Inverter( + InverterParameters( + device_id="inverter1", + max_power_wh=10000, + battery_id="battery1", + ac_charge_limits_total_charge=limits_total, + ), + battery=battery, + ) + sim = GeneticSimulation() + sim.prepare( + GeneticEnergyManagementParameters.model_validate( + dict( + pv_prognose_wh=[8000.0] * hours, + strompreis_euro_pro_wh=[0.0003] * hours, + einspeiseverguetung_euro_pro_wh=0.00008, + preis_euro_pro_wh_akku=0.0001, + gesamtlast=[0.0] * hours, + ) + ), + optimization_hours=config_eos.optimization.genetic.horizon_hours, + prediction_hours=hours, + inverter=inverter, + ev=None, + home_appliance=None, + ) + ac_hours, dc_hours = sim.ac_charge_hours, sim.dc_charge_hours + discharge_hours = sim.bat_discharge_hours + assert ac_hours is not None and dc_hours is not None and discharge_hours is not None + ac_hours[:] = 0 + dc_hours[:] = 0 + discharge_hours[:] = 0 + ac_hours[1] = ac_factor + dc_hours[1] = dc_factor + return sim.simulate(start_hour=0) + + return _simulate + + def test_ac_slot_under_pv_surplus_exports_more_than_dc(self, simulate): + ac = simulate(limits_total=True, ac_factor=0.5, dc_factor=0) + dc = simulate(limits_total=True, ac_factor=0, dc_factor=1) + + assert ac["Netzeinspeisung_Wh_pro_Stunde"][1] == pytest.approx( + dc["Netzeinspeisung_Wh_pro_Stunde"][1] + 0.5 * MAX_CHARGE_W + ) + assert ac["akku_soc_pro_stunde"][2] < dc["akku_soc_pro_stunde"][2] + + def test_default_model_treats_ac_slot_like_dc_under_pv_surplus(self, simulate): + ac = simulate(limits_total=False, ac_factor=0.5, dc_factor=0) + dc = simulate(limits_total=False, ac_factor=0, dc_factor=1) + + assert ac["Netzeinspeisung_Wh_pro_Stunde"][1] == pytest.approx( + dc["Netzeinspeisung_Wh_pro_Stunde"][1] + ) + + +class TestConfigOption: + """The flag is a device config option that reaches the GENETIC optimizer.""" + + @pytest.mark.parametrize("enabled", [False, True]) + def test_config_passes_the_flag_to_the_optimizer(self, enabled): + settings = InverterCommonSettings( + device_id="inverter", + max_power_w=10000, + battery_id="battery1", + ac_charge_limits_total_charge=enabled, + ) + assert settings.to_genetic_param().ac_charge_limits_total_charge is enabled + + def test_default_keeps_the_existing_model(self): + settings = InverterCommonSettings(device_id="inverter", max_power_w=10000) + assert settings.ac_charge_limits_total_charge is False + assert settings.to_genetic_param().ac_charge_limits_total_charge is False