diff --git a/src/gsy_e/gsy_e_core/simulation/simulation.py b/src/gsy_e/gsy_e_core/simulation/simulation.py index da7afd8a4..3bf4fb3a9 100644 --- a/src/gsy_e/gsy_e_core/simulation/simulation.py +++ b/src/gsy_e/gsy_e_core/simulation/simulation.py @@ -24,7 +24,7 @@ import psutil from gsy_framework.constants_limits import ConstSettings, GlobalConfig -from gsy_framework.enums import CoefficientAlgorithm, SpotMarketTypeEnum +from gsy_framework.enums import CoefficientAlgorithm, SpotMarketTypeEnum, SCMSelfConsumptionType from gsy_framework.utils import format_datetime, str_to_pendulum_datetime from pendulum import DateTime, Duration, duration @@ -43,9 +43,10 @@ from gsy_e.gsy_e_core.simulation.time_manager import simulation_time_manager_factory from gsy_e.gsy_e_core.util import NonBlockingConsole from gsy_e.models.area.event_deserializer import deserialize_events_to_areas -from gsy_e.models.area.scm_manager import SCMManager +from gsy_e.models.area.scm_manager import SCMManager, SCMManagerWithoutSurplusTrade from gsy_e.models.config import SimulationConfig + if TYPE_CHECKING: from gsy_e.models.area import Area, AreaBase, CoefficientArea @@ -546,6 +547,34 @@ def _cycle_markets(self, slot_no: int) -> None: global_objects.profiles_handler.current_scm_profiles, ) + def _execute_scm_manager_cycle(self, slot_no: int) -> SCMManager: + if ( + ConstSettings.SCMSettings.SELF_CONSUMPTION_TYPE + == SCMSelfConsumptionType.SIMPLIFIED_COLLECTIVE_SELF_CONSUMPTION_41.value + ): + scm_manager = SCMManagerWithoutSurplusTrade( + self.area, self._get_current_market_time_slot(slot_no) + ) + production_kwh = self.area.aggregate_production_from_all_homes( + self.progress_info.current_slot_time + ) + self.area.calculate_home_after_meter_data_for_collective_self_consumption( + self.progress_info.current_slot_time, scm_manager, production_kwh + ) + else: + scm_manager = SCMManager(self.area, self._get_current_market_time_slot(slot_no)) + self.area.calculate_home_after_meter_data( + self.progress_info.current_slot_time, scm_manager + ) + + scm_manager.calculate_community_after_meter_data() + self.area.trigger_energy_trades(scm_manager) + scm_manager.accumulate_community_trades() + + if ConstSettings.SCMSettings.MARKET_ALGORITHM == CoefficientAlgorithm.DYNAMIC.value: + self.area.change_home_coefficient_percentage(scm_manager) + return scm_manager + def _execute_simulation( self, slot_resume: int, _tick_resume: int, console: NonBlockingConsole = None ) -> None: @@ -566,18 +595,7 @@ def _execute_simulation( self._handle_external_communication() - scm_manager = SCMManager(self.area, self._get_current_market_time_slot(slot_no)) - - self.area.calculate_home_after_meter_data( - self.progress_info.current_slot_time, scm_manager - ) - - scm_manager.calculate_community_after_meter_data() - self.area.trigger_energy_trades(scm_manager) - scm_manager.accumulate_community_trades() - - if ConstSettings.SCMSettings.MARKET_ALGORITHM == CoefficientAlgorithm.DYNAMIC.value: - self.area.change_home_coefficient_percentage(scm_manager) + scm_manager = self._execute_scm_manager_cycle(slot_no) # important: SCM manager has to be updated before sending the results self._results.update_scm_manager(scm_manager) diff --git a/src/gsy_e/models/area/coefficient_area.py b/src/gsy_e/models/area/coefficient_area.py index 6abcffadd..2484ef414 100644 --- a/src/gsy_e/models/area/coefficient_area.py +++ b/src/gsy_e/models/area/coefficient_area.py @@ -16,19 +16,18 @@ along with this program. If not, see . """ -from collections import defaultdict from logging import getLogger from typing import TYPE_CHECKING, List, Dict from numpy.random import random from pendulum import DateTime +from gsy_e.gsy_e_core.util import get_slots_per_month from gsy_e.models.area.area_base import AreaBase +from gsy_e.models.area.scm_dataclasses import SCMAreaProperties from gsy_e.models.config import SimulationConfig from gsy_e.models.strategy.external_strategies import ExternalMixin from gsy_e.models.strategy.scm import SCMStrategy -from gsy_e.models.area.scm_dataclasses import SCMAreaProperties -from gsy_e.gsy_e_core.util import get_slots_per_month log = getLogger(__name__) @@ -131,17 +130,11 @@ def _calculate_home_after_meter_data( home_production_kWh = 0 home_consumption_kWh = 0 - asset_energy_requirements_kWh = defaultdict(lambda: 0) - for child in self.children: # import - consumption_kWh = child.strategy.get_energy_to_buy_kWh(current_time_slot) - asset_energy_requirements_kWh[child.uuid] += consumption_kWh - home_consumption_kWh += consumption_kWh + home_consumption_kWh += child.strategy.get_energy_to_buy_kWh(current_time_slot) # export - production_kWh = child.strategy.get_energy_to_sell_kWh(current_time_slot) - asset_energy_requirements_kWh[child.uuid] -= production_kWh - home_production_kWh += production_kWh + home_production_kWh += child.strategy.get_energy_to_sell_kWh(current_time_slot) scm_manager.add_home_data( self.uuid, @@ -149,9 +142,46 @@ def _calculate_home_after_meter_data( self.area_properties, home_production_kWh, home_consumption_kWh, - dict(asset_energy_requirements_kWh), ) + def aggregate_production_from_all_homes(self, current_time_slot: DateTime) -> float: + """Aggregate energy production from all homes, in kWh.""" + if self.is_home_area: + return sum( + child.strategy.get_energy_to_sell_kWh(current_time_slot) for child in self.children + ) + return sum( + child.aggregate_production_from_all_homes(current_time_slot) + for child in sorted(self.children, key=lambda _: random()) + ) + + def calculate_home_after_meter_data_for_collective_self_consumption( + self, + current_time_slot: DateTime, + scm_manager: "SCMManager", + community_production_kwh: float, + ): + """Recursive function that calculates the home after meter data.""" + if self.is_home_area: + home_consumption_kwh = sum( + child.strategy.get_energy_to_buy_kWh(current_time_slot) for child in self.children + ) + home_production_kwh = ( + community_production_kwh + * self.area_properties.AREA_PROPERTIES["coefficient_percentage"] + ) + scm_manager.add_home_data( + self.uuid, + self.name, + self.area_properties, + home_production_kwh, + home_consumption_kwh, + ) + for child in sorted(self.children, key=lambda _: random()): + child.calculate_home_after_meter_data_for_collective_self_consumption( + current_time_slot, scm_manager, community_production_kwh + ) + def calculate_home_after_meter_data( self, current_time_slot: DateTime, scm_manager: "SCMManager" ) -> None: diff --git a/src/gsy_e/models/area/scm_dataclasses.py b/src/gsy_e/models/area/scm_dataclasses.py index a285aa890..f009c4a42 100644 --- a/src/gsy_e/models/area/scm_dataclasses.py +++ b/src/gsy_e/models/area/scm_dataclasses.py @@ -4,7 +4,6 @@ from typing import Dict, List from uuid import uuid4 -from gsy_framework.constants_limits import is_no_community_self_consumption from gsy_framework.data_classes import Trade, TraderDetails from gsy_framework.sim_results.kpi_calculation_helper import KPICalculationHelper from pendulum import DateTime @@ -45,7 +44,6 @@ class HomeAfterMeterData: _self_production_for_community_kWh: float = 0.0 trades: List[Trade] = None area_properties: SCMAreaProperties = field(default_factory=SCMAreaProperties) - asset_energy_requirements_kWh: Dict[str, float] = field(default_factory=dict) def to_dict(self) -> Dict: """Dict representation of the home after meter data.""" @@ -111,10 +109,6 @@ def set_total_community_production(self, energy_kWh: float): def set_production_for_community(self, unassigned_energy_production_kWh: float): """Assign the energy surplus of the home to be consumed by the community.""" - - if is_no_community_self_consumption(): - self._self_production_for_community_kWh = 0 - return 0.0 if self.energy_surplus_kWh <= unassigned_energy_production_kWh: self._self_production_for_community_kWh = self.energy_surplus_kWh return unassigned_energy_production_kWh - self.energy_surplus_kWh @@ -220,6 +214,15 @@ def create_sell_trade( logging.info("[SCM][TRADE][OFFER] [%s] [%s] %s", self.home_name, trade.time_slot, trade) +class HomeAfterMeterDataWithoutSurplusTrade(HomeAfterMeterData): + """Dedicated HomeAfterMeterData class, specific for the non-suplus trade SCM.""" + + def set_production_for_community(self, unassigned_energy_production_kWh: float): + """Assign the energy surplus of the home to be consumed by the community.""" + self._self_production_for_community_kWh = 0 + return 0.0 + + @dataclass class CommunityData: # pylint: disable=too-many-instance-attributes @@ -442,13 +445,6 @@ def set_min_max_community_savings( @property def savings(self): """Absolute price savings of the home, compared to the base energy bill.""" - # The savings and the percentage might produce negative and huge percentage values - # in cases of production. This is due to the fact that the energy bill in these cases - # will be negative, and the producer will not have "savings". For a more realistic case - # the revenue should be omitted from the calculation of the savings, however this needs - # to be discussed. - if is_no_community_self_consumption(): - return self.self_consumed_savings + self.gsy_energy_bill_revenue savings_absolute = KPICalculationHelper().saving_absolute( self.base_energy_bill_excl_revenue, self.gsy_energy_bill_excl_revenue ) @@ -530,22 +526,35 @@ def calculate_base_energy_bill( self, home_data: HomeAfterMeterData, area_rates: AreaEnergyRates ): """Calculate the base (not with GSy improvements) energy bill for the home.""" - if is_no_community_self_consumption(): - self.base_energy_bill_excl_revenue = ( - home_data.consumption_kWh * area_rates.utility_rate_incl_fees - ) - self.base_energy_bill_revenue = home_data.production_kWh * area_rates.feed_in_tariff - self.base_energy_bill = ( - self.base_energy_bill_excl_revenue - self.base_energy_bill_revenue - ) - else: - self.base_energy_bill_revenue = ( - home_data.energy_surplus_kWh * area_rates.feed_in_tariff - ) - self.base_energy_bill_excl_revenue = ( - home_data.energy_need_kWh * area_rates.utility_rate_incl_fees - + area_rates.area_fees.total_monthly_fees - ) - self.base_energy_bill = ( - self.base_energy_bill_excl_revenue - self.base_energy_bill_revenue - ) + self.base_energy_bill_revenue = home_data.energy_surplus_kWh * area_rates.feed_in_tariff + self.base_energy_bill_excl_revenue = ( + home_data.energy_need_kWh * area_rates.utility_rate_incl_fees + + area_rates.area_fees.total_monthly_fees + ) + self.base_energy_bill = self.base_energy_bill_excl_revenue - self.base_energy_bill_revenue + + +class AreaEnergyBillsWithoutSurplusTrade(AreaEnergyBills): + """Dedicated AreaEnergyBills class, specific for the non-suplus trade SCM.""" + + @property + def savings(self): + """Absolute price savings of the home, compared to the base energy bill.""" + # The savings and the percentage might produce negative and huge percentage values + # in cases of production. This is due to the fact that the energy bill in these cases + # will be negative, and the producer will not have "savings". For a more realistic case + # the revenue should be omitted from the calculation of the savings, however this needs + # to be discussed. + return self.self_consumed_savings + self.gsy_energy_bill_revenue + + def calculate_base_energy_bill( + self, home_data: HomeAfterMeterData, area_rates: AreaEnergyRates + ): + """Calculate the base (not with GSy improvements) energy bill for the home.""" + + self.base_energy_bill_excl_revenue = ( + home_data.consumption_kWh * area_rates.utility_rate_incl_fees + + area_rates.area_fees.total_monthly_fees + ) + self.base_energy_bill_revenue = 0.0 + self.base_energy_bill = self.base_energy_bill_excl_revenue diff --git a/src/gsy_e/models/area/scm_manager.py b/src/gsy_e/models/area/scm_manager.py index c9f76fa16..f14404cc3 100644 --- a/src/gsy_e/models/area/scm_manager.py +++ b/src/gsy_e/models/area/scm_manager.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Dict, Optional from gsy_framework.constants_limits import ConstSettings +from gsy_framework.enums import SCMSelfConsumptionType from pendulum import DateTime import gsy_e.constants @@ -19,6 +20,8 @@ FeeContainer, SCMAreaProperties, AreaEnergyBills, + AreaEnergyBillsWithoutSurplusTrade, + HomeAfterMeterDataWithoutSurplusTrade, ) from gsy_e.models.strategy.scm import SCMStrategy @@ -41,6 +44,15 @@ def __init__(self, area: "CoefficientArea", time_slot: DateTime): self._grid_fees_reduction = ConstSettings.SCMSettings.GRID_FEES_REDUCTION self._intracommunity_base_rate_eur = ConstSettings.SCMSettings.INTRACOMMUNITY_BASE_RATE_EUR + @property + def _home_after_meter_data_class(self) -> type[HomeAfterMeterData]: + if ( + ConstSettings.SCMSettings.SELF_CONSUMPTION_TYPE + == SCMSelfConsumptionType.SIMPLIFIED_COLLECTIVE_SELF_CONSUMPTION_41.value + ): + return HomeAfterMeterDataWithoutSurplusTrade + return HomeAfterMeterData + @staticmethod def _get_community_uuid_from_area(area): # Community is always the root area in the context of SCM. @@ -65,7 +77,6 @@ def add_home_data( area_properties: SCMAreaProperties, production_kWh: float, consumption_kWh: float, - asset_energy_requirements_kWh: Dict[str, float], ): # pylint: disable=too-many-arguments """Import data for one individual home.""" @@ -76,7 +87,6 @@ def add_home_data( area_properties=area_properties, production_kWh=production_kWh, consumption_kWh=consumption_kWh, - asset_energy_requirements_kWh=asset_energy_requirements_kWh, ) def calculate_community_after_meter_data(self): @@ -293,6 +303,71 @@ def community_bills(self) -> Dict: return community_bills.to_dict() +class SCMManagerWithoutSurplusTrade(SCMManager): + """Dedicated SCMManager class, specific for the non-suplus trade SCM.""" + + def add_home_data( + self, + home_uuid: str, + home_name: str, + area_properties: SCMAreaProperties, + production_kWh: float, + consumption_kWh: float, + ): + # pylint: disable=too-many-arguments + """Import data for one individual home.""" + self._home_data[home_uuid] = HomeAfterMeterDataWithoutSurplusTrade( + home_uuid, + home_name, + area_properties=area_properties, + production_kWh=production_kWh, + consumption_kWh=consumption_kWh, + ) + + def calculate_home_energy_bills(self, home_uuid: str) -> None: + """Calculate energy bills for one home.""" + assert home_uuid in self._home_data + home_data = self._home_data[home_uuid] + + area_rates = self._init_area_energy_rates(home_data) + home_bill = AreaEnergyBillsWithoutSurplusTrade( + energy_rates=area_rates, + gsy_energy_bill=area_rates.area_fees.total_monthly_fees, + self_consumed_savings=home_data.self_consumed_energy_kWh + * home_data.area_properties.AREA_PROPERTIES["market_maker_rate"], + ) + home_bill.calculate_base_energy_bill(home_data, area_rates) + + # First handle the sold energy case. This case occurs in case of energy surplus of a home. + if home_data.energy_surplus_kWh > 0.0: + home_bill.set_sold_to_grid( + home_data.energy_surplus_kWh, + home_data.area_properties.AREA_PROPERTIES["feed_in_tariff"], + ) + home_bill.set_export_grid_fees(home_data.energy_surplus_kWh) + if home_data.energy_surplus_kWh > FLOATING_POINT_TOLERANCE: + home_data.create_sell_trade( + self._time_slot, + DEFAULT_SCM_GRID_NAME, + home_data.energy_surplus_kWh, + home_data.energy_surplus_kWh + * home_data.area_properties.AREA_PROPERTIES["feed_in_tariff"], + ) + + if home_data.energy_need_kWh > 0.0: + home_bill.set_bought_from_grid(home_data.energy_need_kWh) + + if home_data.energy_need_kWh > FLOATING_POINT_TOLERANCE: + home_data.create_buy_trade( + self._time_slot, + DEFAULT_SCM_GRID_NAME, + home_data.energy_need_kWh, + home_data.energy_need_kWh * area_rates.utility_rate_incl_fees, + ) + + self._bills[home_uuid] = home_bill + + class SCMCommunityValidator: """Validator for SCM Community areas.""" diff --git a/src/gsy_e/setup/gsy_e_settings.json b/src/gsy_e/setup/gsy_e_settings.json index 42874bdbb..4688e1ac1 100644 --- a/src/gsy_e/setup/gsy_e_settings.json +++ b/src/gsy_e/setup/gsy_e_settings.json @@ -3,7 +3,7 @@ "sim_duration": "24h", "slot_length": "15m", "tick_length": "15s", - "start_date": "2024-08-26" + "start_date": "2024-10-30" }, "advanced_settings": { "GeneralSettings": { diff --git a/tests/area/test_coefficient_area.py b/tests/area/test_coefficient_area.py index e4c92b24e..8de718d0e 100644 --- a/tests/area/test_coefficient_area.py +++ b/tests/area/test_coefficient_area.py @@ -22,13 +22,23 @@ import pytest from gsy_framework.constants_limits import ConstSettings -from gsy_framework.enums import SpotMarketTypeEnum, CoefficientAlgorithm, SCMPropertyType +from gsy_framework.enums import ( + SpotMarketTypeEnum, + CoefficientAlgorithm, + SCMPropertyType, + SCMSelfConsumptionType, +) from pendulum import duration, today from pendulum import now from gsy_e import constants from gsy_e.models.area import CoefficientArea, CoefficientAreaException -from gsy_e.models.area.scm_manager import SCMManager, HomeAfterMeterData, AreaEnergyBills +from gsy_e.models.area.scm_manager import ( + SCMManager, + HomeAfterMeterData, + AreaEnergyBills, + SCMManagerWithoutSurplusTrade, +) from gsy_e.models.config import SimulationConfig from gsy_e.models.strategy.scm.load import SCMLoadHoursStrategy from gsy_e.models.strategy.scm.pv import SCMPVUserProfile @@ -59,6 +69,9 @@ def setup_method(): @staticmethod def teardown_method(): ConstSettings.SCMSettings.MARKET_ALGORITHM = 3 + ConstSettings.SCMSettings.SELF_CONSUMPTION_TYPE = ( + SCMSelfConsumptionType.COLLECTIVE_SELF_CONSUMPTION_SURPLUS_42.value + ) ConstSettings.MASettings.MARKET_TYPE = SpotMarketTypeEnum.ONE_SIDED.value @staticmethod @@ -436,3 +449,105 @@ def test_virtual_compensation_is_calculated_correctly(_create_2_house_grid): assert isclose(scm._bills[house1.uuid].export_grid_fees, 0) assert isclose(scm._bills[house2.uuid].export_grid_fees, 0.0004) + + @staticmethod + def test_simplified_self_consumption_energy_results(_create_2_house_grid): + ConstSettings.SCMSettings.SELF_CONSUMPTION_TYPE = ( + SCMSelfConsumptionType.SIMPLIFIED_COLLECTIVE_SELF_CONSUMPTION_41.value + ) + grid_area = _create_2_house_grid + house1 = grid_area.children[0] + house2 = grid_area.children[1] + house1.area_properties.AREA_PROPERTIES["coefficient_percentage"] = 0.8 + house2.area_properties.AREA_PROPERTIES["coefficient_percentage"] = 0.2 + time_slot = now() + scm = SCMManagerWithoutSurplusTrade(grid_area, time_slot) + production = grid_area.aggregate_production_from_all_homes(time_slot) + assert production == 0.7 + grid_area.calculate_home_after_meter_data_for_collective_self_consumption( + time_slot, scm, production + ) + + assert isclose(scm._home_data[house1.uuid].production_kWh, 0.7 * 0.8) + assert isclose(scm._home_data[house2.uuid].production_kWh, 0.7 * 0.2) + + assert isclose(scm._home_data[house1.uuid].consumption_kWh, 0.7) + assert isclose(scm._home_data[house2.uuid].consumption_kWh, 0.1) + + assert isclose(scm._home_data[house1.uuid].self_consumed_energy_kWh, 0.7 * 0.8) + assert isclose(scm._home_data[house2.uuid].self_consumed_energy_kWh, 0.1) + + assert isclose(scm._home_data[house1.uuid].energy_surplus_kWh, 0.0) + assert isclose(scm._home_data[house2.uuid].energy_surplus_kWh, 0.7 * 0.2 - 0.1) + + assert isclose(scm._home_data[house1.uuid].energy_need_kWh, 0.7 - 0.7 * 0.8) + assert isclose(scm._home_data[house2.uuid].energy_need_kWh, 0.0) + + assert isclose(scm._home_data[house1.uuid].energy_bought_from_community_kWh, 0.0) + assert isclose(scm._home_data[house1.uuid].energy_sold_to_grid_kWh, 0.0) + assert isclose(scm._home_data[house1.uuid].self_production_for_community_kWh, 0.0) + assert isclose(scm._home_data[house1.uuid].self_production_for_grid_kWh, 0.0) + + assert isclose(scm._home_data[house2.uuid].energy_bought_from_community_kWh, 0.0) + assert isclose(scm._home_data[house2.uuid].energy_sold_to_grid_kWh, 0.7 * 0.2 - 0.1) + assert isclose(scm._home_data[house2.uuid].self_production_for_community_kWh, 0.0) + assert isclose(scm._home_data[house2.uuid].self_production_for_grid_kWh, 0.7 * 0.2 - 0.1) + + @staticmethod + def test_simplified_self_consumption_bills_results(_create_2_house_grid): + # pylint: disable=too-many-locals + ConstSettings.SCMSettings.SELF_CONSUMPTION_TYPE = ( + SCMSelfConsumptionType.SIMPLIFIED_COLLECTIVE_SELF_CONSUMPTION_41.value + ) + grid_area = _create_2_house_grid + house1 = grid_area.children[0] + house2 = grid_area.children[1] + house1.area_properties.AREA_PROPERTIES["coefficient_percentage"] = 0.8 + house2.area_properties.AREA_PROPERTIES["coefficient_percentage"] = 0.2 + time_slot = now() + scm = SCMManagerWithoutSurplusTrade(grid_area, time_slot) + production = grid_area.aggregate_production_from_all_homes(time_slot) + grid_area.calculate_home_after_meter_data_for_collective_self_consumption( + time_slot, scm, production + ) + scm.calculate_community_after_meter_data() + grid_area.trigger_energy_trades(scm) + scm.accumulate_community_trades() + + rate_per_kwh1 = scm._bills[house1.uuid].energy_rates.utility_rate + rate_fees_per_kwh1 = scm._bills[house1.uuid].energy_rates.utility_rate_incl_fees + monthly_fees1 = scm._bills[house1.uuid].energy_rates.area_fees.total_monthly_fees + expected_bill1 = 0.7 * rate_fees_per_kwh1 + monthly_fees1 + assert isclose(scm._bills[house1.uuid].base_energy_bill, expected_bill1) + assert isclose(scm._bills[house1.uuid].base_energy_bill_excl_revenue, expected_bill1) + assert isclose(scm._bills[house1.uuid].base_energy_bill_revenue, 0.0) + expected_consumption_from_grid1 = 0.7 - 0.7 * 0.8 + expected_gsy_bill1 = expected_consumption_from_grid1 * rate_fees_per_kwh1 + monthly_fees1 + assert isclose(scm._bills[house1.uuid].gsy_energy_bill, expected_gsy_bill1) + assert isclose(scm._bills[house1.uuid].gsy_energy_bill_excl_revenue, expected_gsy_bill1) + assert isclose(scm._bills[house1.uuid].gsy_energy_bill_revenue, 0.0) + assert isclose( + scm._bills[house1.uuid].gsy_energy_bill_excl_revenue_without_fees, + expected_consumption_from_grid1 * rate_fees_per_kwh1, + ) + assert isclose( + scm._bills[house1.uuid].gsy_energy_bill_excl_fees, + expected_consumption_from_grid1 * rate_per_kwh1, + ) + + feed_in_tariff2 = scm._bills[house2.uuid].energy_rates.feed_in_tariff + rate_fees_per_kwh2 = scm._bills[house2.uuid].energy_rates.utility_rate_incl_fees + monthly_fees2 = scm._bills[house2.uuid].energy_rates.area_fees.total_monthly_fees + expected_bill2 = 0.1 * rate_fees_per_kwh2 + monthly_fees2 + assert isclose(scm._bills[house2.uuid].base_energy_bill, expected_bill2) + assert isclose(scm._bills[house2.uuid].base_energy_bill_excl_revenue, expected_bill2) + assert isclose(scm._bills[house2.uuid].base_energy_bill_revenue, 0.0) + expected_production_for_grid2 = 0.7 * 0.2 - 0.1 + expected_gsy_bill2 = -expected_production_for_grid2 * feed_in_tariff2 + monthly_fees2 + assert isclose(scm._bills[house2.uuid].gsy_energy_bill, expected_gsy_bill2) + assert isclose(scm._bills[house2.uuid].gsy_energy_bill_excl_revenue, monthly_fees2) + assert isclose(scm._bills[house2.uuid].gsy_energy_bill_revenue, abs(expected_gsy_bill2)) + assert isclose(scm._bills[house2.uuid].gsy_energy_bill_excl_revenue_without_fees, 0.0) + assert isclose( + scm._bills[house2.uuid].gsy_energy_bill_excl_fees, expected_gsy_bill2 - monthly_fees2 + )