diff --git a/src/gsy_e/constants.py b/src/gsy_e/constants.py index 6e6bc627a..82e77aa38 100644 --- a/src/gsy_e/constants.py +++ b/src/gsy_e/constants.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # Need to import required settings from gsy-framework in order to be available in d3a, # thus avoiding accessing the gsy-framework constants. # pylint: disable=unused-import @@ -66,14 +67,19 @@ CONNECT_TO_PROFILES_DB = False SEND_EVENTS_RESPONSES_TO_SDK_VIA_RQ = False +RUN_IN_NON_P2P_MODE = False + DEFAULT_SCM_COMMUNITY_NAME = "Community" DEFAULT_SCM_GRID_NAME = "Grid" FORWARD_MARKET_MAX_DURATION_YEARS = 6 +MIN_OFFER_BID_AGE_P2P_DISABLED = 360 + class SettlementTemplateStrategiesConstants: """Constants related to the configuration of settlement template strategies""" + INITIAL_BUYING_RATE = 0 FINAL_BUYING_RATE = 50 INITIAL_SELLING_RATE = 50 diff --git a/src/gsy_e/gsy_e_core/non_p2p_handler.py b/src/gsy_e/gsy_e_core/non_p2p_handler.py new file mode 100644 index 000000000..41404db09 --- /dev/null +++ b/src/gsy_e/gsy_e_core/non_p2p_handler.py @@ -0,0 +1,54 @@ +from gsy_framework.constants_limits import GlobalConfig +from gsy_framework.exceptions import GSyException + + +class NonP2PHandler: + """Handles non-p2p case""" + + def __init__(self, scenario: dict): + self.non_p2p_scenario = scenario + self._energy_sell_rate = 0.0 + self._energy_buy_rate = 0.0 + self._get_energy_rates_from_infinite_bus(scenario) + self._handle_non_p2p_scenario(scenario) + + def _get_energy_rates_from_infinite_bus(self, scenario: dict): + for child in scenario["children"]: + if child.get("type") == "InfiniteBus": + self._energy_buy_rate = child.get("energy_buy_rate", GlobalConfig.FEED_IN_TARIFF) + self._energy_sell_rate = child.get( + "energy_sell_rate", GlobalConfig.MARKET_MAKER_RATE + ) + return + + raise GSyException( + "For non-p2p simulation, an InfiniteBus has to be present in the first " + "level of the configuration tree." + ) + + @staticmethod + def _is_home_area(area: dict): + return area.get("children") and all( + child.get("type", None) for child in area.get("children") + ) + + def _add_market_maker_to_home(self, area: dict): + if "children" not in area or not area["children"]: + return + if not self._is_home_area(area): + return + area["children"].append( + { + "name": "MarketMaker", + "type": "InfiniteBus", + "energy_buy_rate": self._energy_buy_rate, + "energy_sell_rate": self._energy_sell_rate, + } + ) + + def _handle_non_p2p_scenario(self, area: dict): + if "children" not in area or not area["children"]: + return + self._add_market_maker_to_home(area) + for child in area["children"]: + self._handle_non_p2p_scenario(child) diff --git a/src/gsy_e/gsy_e_core/rq_job_handler.py b/src/gsy_e/gsy_e_core/rq_job_handler.py index 14bf1480a..bf8224f28 100644 --- a/src/gsy_e/gsy_e_core/rq_job_handler.py +++ b/src/gsy_e/gsy_e_core/rq_job_handler.py @@ -14,6 +14,7 @@ from gsy_e.gsy_e_core.simulation import run_simulation from gsy_e.gsy_e_core.util import update_advanced_settings from gsy_e.models.config import SimulationConfig +from gsy_e.gsy_e_core.non_p2p_handler import NonP2PHandler logging.getLogger().setLevel(logging.ERROR) logger = logging.getLogger(__name__) @@ -21,24 +22,31 @@ # pylint: disable=too-many-branches, too-many-statements -def launch_simulation_from_rq_job(scenario: Dict, - settings: Optional[Dict], - events: Optional[str], - aggregator_device_mapping: Dict, - saved_state: Dict, - scm_properties: Dict, - job_id: str, - connect_to_profiles_db: bool = True): +def launch_simulation_from_rq_job( + scenario: Dict, + settings: Optional[Dict], + events: Optional[str], + aggregator_device_mapping: Dict, + saved_state: Dict, + scm_properties: Dict, + job_id: str, + connect_to_profiles_db: bool = True, +): # pylint: disable=too-many-arguments, too-many-locals """Launch simulation from rq job.""" gsy_e.constants.CONFIGURATION_ID = scenario.pop("configuration_uuid", None) try: if not gsy_e.constants.CONFIGURATION_ID: - raise Exception("configuration_uuid was not provided") + raise Exception( + "configuration_uuid was not provided" + ) # pylint disable=broad-exception-raised - logger.error("Starting simulation with job_id: %s and configuration id: %s", - job_id, gsy_e.constants.CONFIGURATION_ID) + logger.error( + "Starting simulation with job_id: %s and configuration id: %s", + job_id, + gsy_e.constants.CONFIGURATION_ID, + ) settings = _adapt_settings(settings) @@ -47,21 +55,33 @@ def launch_simulation_from_rq_job(scenario: Dict, _configure_constants_constsettings(scenario, settings, connect_to_profiles_db) + if gsy_e.constants.RUN_IN_NON_P2P_MODE: + scenario = NonP2PHandler(scenario).non_p2p_scenario + slot_length_realtime = ( duration(seconds=settings["slot_length_realtime"].seconds) - if "slot_length_realtime" in settings else None) + if "slot_length_realtime" in settings + else None + ) scenario_name = "json_arg" - kwargs = {"no_export": True, - "seed": settings.get("random_seed", 0)} + kwargs = {"no_export": True, "seed": settings.get("random_seed", 0)} if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.COEFFICIENTS.value: kwargs.update({"scm_properties": scm_properties}) past_slots_sim_state = _handle_scm_past_slots_simulation_run( - scenario, settings, events, aggregator_device_mapping, saved_state, job_id, - scenario_name, slot_length_realtime, kwargs) + scenario, + settings, + events, + aggregator_device_mapping, + saved_state, + job_id, + scenario_name, + slot_length_realtime, + kwargs, + ) if past_slots_sim_state is not None: saved_state = past_slots_sim_state @@ -69,39 +89,48 @@ def launch_simulation_from_rq_job(scenario: Dict, # facilitate the state resume. saved_state["general"]["sim_status"] = "running" - config = _create_config_settings_object( - scenario, settings, aggregator_device_mapping) + config = _create_config_settings_object(scenario, settings, aggregator_device_mapping) if settings.get("type") == ConfigurationType.CANARY_NETWORK.value: - config.start_date = ( - instance( - datetime.combine(date.today(), datetime.min.time()), - tz=gsy_e.constants.TIME_ZONE)) + config.start_date = instance( + datetime.combine(date.today(), datetime.min.time()), tz=gsy_e.constants.TIME_ZONE + ) if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.COEFFICIENTS.value: - config.start_date = config.start_date.subtract(hours=settings["scm"] - ["scm_cn_hours_of_delay"]) - - run_simulation(setup_module_name=scenario_name, - simulation_config=config, - simulation_events=events, - redis_job_id=job_id, - saved_sim_state=saved_state, - slot_length_realtime=slot_length_realtime, - kwargs=kwargs) - - logger.info("Finishing simulation with job_id: %s and configuration id: %s", - job_id, gsy_e.constants.CONFIGURATION_ID) + config.start_date = config.start_date.subtract( + hours=settings["scm"]["scm_cn_hours_of_delay"] + ) + + run_simulation( + setup_module_name=scenario_name, + simulation_config=config, + simulation_events=events, + redis_job_id=job_id, + saved_sim_state=saved_state, + slot_length_realtime=slot_length_realtime, + kwargs=kwargs, + ) + + logger.info( + "Finishing simulation with job_id: %s and configuration id: %s", + job_id, + gsy_e.constants.CONFIGURATION_ID, + ) # pylint: disable=broad-except except Exception: # pylint: disable=import-outside-toplevel from gsy_e.gsy_e_core.redis_connections.simulation import publish_job_error_output - logger.error("Error on jobId, %s, configuration id: %s", - job_id, gsy_e.constants.CONFIGURATION_ID) + + logger.error( + "Error on jobId, %s, configuration id: %s", job_id, gsy_e.constants.CONFIGURATION_ID + ) publish_job_error_output(job_id, traceback.format_exc()) - logger.error("Error on jobId, %s, configuration id: %s: error sent to gsy-web", - job_id, gsy_e.constants.CONFIGURATION_ID) + logger.error( + "Error on jobId, %s, configuration id: %s: error sent to gsy-web", + job_id, + gsy_e.constants.CONFIGURATION_ID, + ) raise @@ -119,7 +148,8 @@ def _adapt_settings(settings: Dict) -> Dict: def _configure_constants_constsettings( - scenario: Dict, settings: Dict, connect_to_profiles_db: bool): + scenario: Dict, settings: Dict, connect_to_profiles_db: bool +): assert isinstance(scenario, dict) GlobalConfig.CONFIG_TYPE = settings.get("type") @@ -127,11 +157,14 @@ def _configure_constants_constsettings( if settings.get("type") == ConfigurationType.COLLABORATION.value: gsy_e.constants.EXTERNAL_CONNECTION_WEB = True - if settings.get("type") in [ConfigurationType.CANARY_NETWORK.value, - ConfigurationType.B2B.value]: + if settings.get("type") in [ + ConfigurationType.CANARY_NETWORK.value, + ConfigurationType.B2B.value, + ]: gsy_e.constants.EXTERNAL_CONNECTION_WEB = True gsy_e.constants.RUN_IN_REALTIME = ( - settings.get("type") == ConfigurationType.CANARY_NETWORK.value) + settings.get("type") == ConfigurationType.CANARY_NETWORK.value + ) if settings.get("type") == ConfigurationType.B2B.value: ConstSettings.ForwardMarketSettings.ENABLE_FORWARD_MARKETS = True @@ -149,30 +182,36 @@ def _configure_constants_constsettings( if bid_offer_match_algo: ConstSettings.MASettings.BID_OFFER_MATCH_TYPE = bid_offer_match_algo - ConstSettings.SettlementMarketSettings.RELATIVE_STD_FROM_FORECAST_FLOAT = ( - settings.get( - "relative_std_from_forecast_percent", - ConstSettings.SettlementMarketSettings.RELATIVE_STD_FROM_FORECAST_FLOAT - )) + ConstSettings.SettlementMarketSettings.RELATIVE_STD_FROM_FORECAST_FLOAT = settings.get( + "relative_std_from_forecast_percent", + ConstSettings.SettlementMarketSettings.RELATIVE_STD_FROM_FORECAST_FLOAT, + ) ConstSettings.SettlementMarketSettings.ENABLE_SETTLEMENT_MARKETS = settings.get( "settlement_market_enabled", - ConstSettings.SettlementMarketSettings.ENABLE_SETTLEMENT_MARKETS + ConstSettings.SettlementMarketSettings.ENABLE_SETTLEMENT_MARKETS, ) gsy_e.constants.CONNECT_TO_PROFILES_DB = connect_to_profiles_db + if settings.get("p2p_enabled", True) is False: + ConstSettings.MASettings.MIN_BID_AGE = gsy_e.constants.MIN_OFFER_BID_AGE_P2P_DISABLED + ConstSettings.MASettings.MIN_OFFER_AGE = gsy_e.constants.MIN_OFFER_BID_AGE_P2P_DISABLED + gsy_e.constants.RUN_IN_NON_P2P_MODE = True + if settings.get("scm"): ConstSettings.SCMSettings.MARKET_ALGORITHM = CoefficientAlgorithm( - settings["scm"]["coefficient_algorithm"]).value + settings["scm"]["coefficient_algorithm"] + ).value ConstSettings.SCMSettings.GRID_FEES_REDUCTION = settings["scm"]["grid_fees_reduction"] - ConstSettings.SCMSettings.INTRACOMMUNITY_BASE_RATE_EUR = ( - settings["scm"]["intracommunity_rate_base_eur"]) + ConstSettings.SCMSettings.INTRACOMMUNITY_BASE_RATE_EUR = settings["scm"][ + "intracommunity_rate_base_eur" + ] else: assert spot_market_type is not SpotMarketTypeEnum.COEFFICIENTS.value def _create_config_settings_object( - scenario: Dict, settings: Dict, aggregator_device_mapping: Dict + scenario: Dict, settings: Dict, aggregator_device_mapping: Dict ) -> SimulationConfig: config_settings = { @@ -202,13 +241,9 @@ def _create_config_settings_object( "market_maker_rate": settings.get( "market_maker_rate", ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE ), - "capacity_kW": settings.get( - "capacity_kW", ConstSettings.PVSettings.DEFAULT_CAPACITY_KW - ), + "capacity_kW": settings.get("capacity_kW", ConstSettings.PVSettings.DEFAULT_CAPACITY_KW), "grid_fee_type": settings.get("grid_fee_type", GlobalConfig.grid_fee_type), - "external_connection_enabled": settings.get( - "external_connection_enabled", False - ), + "external_connection_enabled": settings.get("external_connection_enabled", False), "aggregator_device_mapping": aggregator_device_mapping, "hours_of_delay": settings.get("scm", {}).get( "hours_of_delay", ConstSettings.SCMSettings.HOURS_OF_DELAY @@ -222,10 +257,15 @@ def _create_config_settings_object( def _handle_scm_past_slots_simulation_run( - scenario: Dict, settings: Optional[Dict], events: Optional[str], - aggregator_device_mapping: Dict, saved_state: Dict, job_id: str, - scenario_name: str, slot_length_realtime: Optional[duration], - kwargs: Dict + scenario: Dict, + settings: Optional[Dict], + events: Optional[str], + aggregator_device_mapping: Dict, + saved_state: Dict, + job_id: str, + scenario_name: str, + slot_length_realtime: Optional[duration], + kwargs: Dict, ) -> Optional[Dict]: # pylint: disable=too-many-arguments """ @@ -243,16 +283,15 @@ def _handle_scm_past_slots_simulation_run( settings_copy = deepcopy(settings) config = _create_config_settings_object( - scenario_copy, settings_copy, aggregator_device_mapping) + scenario_copy, settings_copy, aggregator_device_mapping + ) # We are running SCM Canary Networks with some days of delay compared to realtime in order to # compensate for delays in transmission of the asset measurements. # Adding 4 hours of extra time to the SCM past slots simulation duration, in order to # compensate for the runtime of the SCM past slots simulation and to not have any results gaps # after this simulation run and the following Canary Network launch. config.end_date = ( - now(tz=gsy_e.constants.TIME_ZONE) - .subtract(hours=config.hours_of_delay) - .add(hours=4) + now(tz=gsy_e.constants.TIME_ZONE).subtract(hours=config.hours_of_delay).add(hours=4) ) config.sim_duration = config.end_date - config.start_date GlobalConfig.sim_duration = config.sim_duration @@ -264,6 +303,7 @@ def _handle_scm_past_slots_simulation_run( redis_job_id=job_id, saved_sim_state=saved_state, slot_length_realtime=slot_length_realtime, - kwargs=kwargs) + kwargs=kwargs, + ) gsy_e.constants.RUN_IN_REALTIME = True return simulation_state diff --git a/src/gsy_e/gsy_e_core/sim_results/endpoint_buffer.py b/src/gsy_e/gsy_e_core/sim_results/endpoint_buffer.py index e3cac55b5..d63e87d2b 100644 --- a/src/gsy_e/gsy_e_core/sim_results/endpoint_buffer.py +++ b/src/gsy_e/gsy_e_core/sim_results/endpoint_buffer.py @@ -32,6 +32,7 @@ from gsy_framework.utils import get_json_dict_memory_allocation_size from pendulum import DateTime +import gsy_e.constants from gsy_e.gsy_e_core.sim_results.offer_bids_trades_hr_stats import OfferBidTradeGraphStats from gsy_e.gsy_e_core.util import ( get_feed_in_tariff_rate_from_config, @@ -237,6 +238,8 @@ def _structure_results_from_area_object(target_area: "AreaBase") -> Dict: area_dict = {} area_dict["name"] = target_area.name area_dict["uuid"] = target_area.uuid + if target_area.is_home_area and gsy_e.constants.RUN_IN_NON_P2P_MODE: + area_dict["non_p2p"] = True area_dict["parent_uuid"] = ( target_area.parent.uuid if target_area.parent is not None else "" ) diff --git a/src/gsy_e/models/area/area_base.py b/src/gsy_e/models/area/area_base.py index 078d22c55..cc99af462 100644 --- a/src/gsy_e/models/area/area_base.py +++ b/src/gsy_e/models/area/area_base.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from logging import getLogger from typing import TYPE_CHECKING, List, Union, Optional from uuid import uuid4 @@ -80,16 +81,19 @@ class AreaBase: Base class for the Area model. Contains common behavior for both coefficient trading and market trading. """ + # pylint: disable=too-many-arguments,too-many-instance-attributes - def __init__(self, name: str = None, - children: List["Area"] = None, - uuid: str = None, - strategy: Optional[Union["BaseStrategy", "TradingStrategyBase"]] = None, - config: SimulationConfig = None, - grid_fee_percentage: float = None, - grid_fee_constant: float = None): - validate_area(grid_fee_constant=grid_fee_constant, - grid_fee_percentage=grid_fee_percentage) + def __init__( + self, + name: str = None, + children: List["Area"] = None, + uuid: str = None, + strategy: Optional[Union["BaseStrategy", "TradingStrategyBase"]] = None, + config: SimulationConfig = None, + grid_fee_percentage: float = None, + grid_fee_constant: float = None, + ): + validate_area(grid_fee_constant=grid_fee_constant, grid_fee_percentage=grid_fee_percentage) self.active = False self.log = TaggedLogWrapper(log, name) self.__name = name @@ -100,9 +104,8 @@ def __init__(self, name: str = None, children = [] children = [child for child in children if child is not None] self.children = ( - AreaChildrenList(self, children) - if children is not None - else AreaChildrenList(self)) + AreaChildrenList(self, children) if children is not None else AreaChildrenList(self) + ) for child in self.children: child.parent = self @@ -124,9 +127,11 @@ def trades(self) -> List["Trade"]: return self.strategy.trades def _set_grid_fees(self, grid_fee_const, grid_fee_percentage): - grid_fee_type = self.config.grid_fee_type \ - if self.config is not None \ + grid_fee_type = ( + self.config.grid_fee_type + if self.config is not None else ConstSettings.MASettings.GRID_FEE_TYPE + ) if grid_fee_type == 1: grid_fee_percentage = None elif grid_fee_type == 2: @@ -165,8 +170,10 @@ def get_path_to_root_fees(self) -> float: def get_grid_fee(self): """Return the current grid fee for the area.""" grid_fee_type = ( - self.config.grid_fee_type if self.config is not None - else ConstSettings.MASettings.GRID_FEE_TYPE) + self.config.grid_fee_type + if self.config is not None + else ConstSettings.MASettings.GRID_FEE_TYPE + ) return self.grid_fee_constant if grid_fee_type == 1 else self.grid_fee_percentage @@ -202,15 +209,18 @@ def area_reconfigure_event(self, **kwargs): grid_fee_constant = ( kwargs["grid_fee_constant"] if key_in_dict_and_not_none(kwargs, "grid_fee_constant") - else self.grid_fee_constant) + else self.grid_fee_constant + ) grid_fee_percentage = ( kwargs["grid_fee_percentage"] if key_in_dict_and_not_none(kwargs, "grid_fee_percentage") - else self.grid_fee_percentage) + else self.grid_fee_percentage + ) try: - validate_area(grid_fee_constant=grid_fee_constant, - grid_fee_percentage=grid_fee_percentage) + validate_area( + grid_fee_constant=grid_fee_constant, grid_fee_percentage=grid_fee_percentage + ) except (GSyAreaException, GSyDeviceException) as ex: log.error(ex) @@ -228,10 +238,14 @@ def update_descendants_strategy_prices(self): child.update_descendants_strategy_prices() except GSyException: log.exception("area.update_descendants_strategy_prices failed.") - return def get_results_dict(self): """Calculate the results dict for the coefficients trading.""" if self.strategy is not None: return self.strategy.state.get_results_dict(self.current_market_time_slot) return {} + + @property + def is_home_area(self): + "Return if the area is a home area." + return self.children and all(child.strategy for child in self.children) diff --git a/src/gsy_e/models/area/coefficient_area.py b/src/gsy_e/models/area/coefficient_area.py index b08597c9a..e553d69d9 100644 --- a/src/gsy_e/models/area/coefficient_area.py +++ b/src/gsy_e/models/area/coefficient_area.py @@ -96,9 +96,6 @@ def area_reconfigure_event(self, **kwargs): if self.strategy is not None: self.strategy.area_reconfigure_event(**kwargs) - def _is_home_area(self): - return self.children and all(child.strategy for child in self.children) - def _calculate_home_after_meter_data( self, current_time_slot: DateTime, scm_manager: "SCMManager" ) -> None: @@ -130,14 +127,14 @@ def calculate_home_after_meter_data( self, current_time_slot: DateTime, scm_manager: "SCMManager" ) -> None: """Recursive function that calculates the home after meter data.""" - if self._is_home_area(): + if self.is_home_area: self._calculate_home_after_meter_data(current_time_slot, scm_manager) for child in sorted(self.children, key=lambda _: random()): child.calculate_home_after_meter_data(current_time_slot, scm_manager) def trigger_energy_trades(self, scm_manager: "SCMManager") -> None: """Recursive function that triggers energy trading on all children of the root area.""" - if self._is_home_area(): + if self.is_home_area: scm_manager.calculate_home_energy_bills(self.uuid) for child in sorted(self.children, key=lambda _: random()): child.trigger_energy_trades(scm_manager) @@ -159,7 +156,7 @@ def change_home_coefficient_percentage(self, scm_manager: "SCMManager") -> None: """Recursive function that change home coefficient percentage based on energy need. This method is for dynamic energy allocation algorithm. """ - if self._is_home_area(): + if self.is_home_area: self._change_home_coefficient_percentage(scm_manager) for child in self.children: child.change_home_coefficient_percentage(scm_manager) diff --git a/tests/test_gsy_core/test_non_p2p_handler.py b/tests/test_gsy_core/test_non_p2p_handler.py new file mode 100644 index 000000000..094c91529 --- /dev/null +++ b/tests/test_gsy_core/test_non_p2p_handler.py @@ -0,0 +1,75 @@ +from gsy_e.gsy_e_core.non_p2p_handler import NonP2PHandler + +SCENARIO = { + "name": "Grid", + "children": [ + { + "name": "InfiniteBus", + "type": "InfiniteBus", + "energy_sell_rate": 31.0, + "energy_buy_rate": 15.0, + }, + { + "name": "Community", + "children": [ + { + "name": "House 1", + "children": [{"name": "Load", "type": "Load"}, {"name": "PV", "type": "PV"}], + }, + { + "name": "House 2", + "children": [{"name": "Load", "type": "Load"}, {"name": "PV", "type": "PV"}], + }, + ], + }, + ], +} + + +class TestNonP2PHandler: + + @staticmethod + def test_handle_non_p2p_scenario_adds_market_makers_to_homes(): + handler = NonP2PHandler(SCENARIO) + assert handler.non_p2p_scenario == { + "name": "Grid", + "children": [ + { + "name": "InfiniteBus", + "type": "InfiniteBus", + "energy_sell_rate": 31.0, + "energy_buy_rate": 15.0, + }, + { + "name": "Community", + "children": [ + { + "name": "House 1", + "children": [ + {"name": "Load", "type": "Load"}, + {"name": "PV", "type": "PV"}, + { + "name": "MarketMaker", + "type": "InfiniteBus", + "energy_buy_rate": 15.0, + "energy_sell_rate": 31.0, + }, + ], + }, + { + "name": "House 2", + "children": [ + {"name": "Load", "type": "Load"}, + {"name": "PV", "type": "PV"}, + { + "name": "MarketMaker", + "type": "InfiniteBus", + "energy_buy_rate": 15.0, + "energy_sell_rate": 31.0, + }, + ], + }, + ], + }, + ], + } diff --git a/tests/test_stats.py b/tests/test_stats.py index 6837dc813..a9fa1f96a 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=protected-access from math import isclose from unittest.mock import MagicMock, Mock @@ -23,8 +24,10 @@ import pytest from gsy_framework.data_classes import Trade, TraderDetails from gsy_framework.sim_results.bills import MarketEnergyBills -from gsy_framework.unit_test_utils import assert_dicts_identical, \ - assert_lists_contain_same_elements +from gsy_framework.unit_test_utils import ( + assert_dicts_identical, + assert_lists_contain_same_elements, +) from pendulum import today, now from gsy_e import constants @@ -40,6 +43,7 @@ def fixture_auto(): class FakeArea: """Fake class that mimics the Area class.""" + # pylint: disable=missing-function-docstring,too-many-instance-attributes def __init__(self, name, children=None, past_markets=None): if not children: @@ -58,6 +62,7 @@ def __init__(self, name, children=None, past_markets=None): self.stats.imported_energy = Mock() self.stats.exported_energy = Mock() self.name_uuid_mapping = {} + self.is_home_area = False @property def future_markets(self): @@ -83,6 +88,7 @@ def get_results_dict(): class FakeMarket: """Fake class that mimics the Market class.""" + # pylint: disable=too-many-instance-attributes def __init__(self, trades, name="Area", fees=0.0): self.name = name @@ -90,15 +96,18 @@ def __init__(self, trades, name="Area", fees=0.0): self.time_slot = today(tz=constants.TIME_ZONE) self.market_fee = fees self.const_fee_rate = fees - self.time_slot_str = self.time_slot.format(constants.DATE_TIME_FORMAT) \ - if self.time_slot is not None \ + self.time_slot_str = ( + self.time_slot.format(constants.DATE_TIME_FORMAT) + if self.time_slot is not None else None + ) self.offer_history = [] self.bid_history = [] class FakeOffer: """Fake class that mimics the Offer class.""" + def __init__(self, price, energy, seller): self.price = price self.energy = energy @@ -111,19 +120,29 @@ def serializable_dict(self): return self.__dict__ -def _trade(price, buyer, energy=1, seller=None, fee_price=0.): - return Trade("id", now(tz=constants.TIME_ZONE), - TraderDetails(seller, ""), TraderDetails(buyer, ""), - energy, price, offer=FakeOffer(price, energy, seller), - fee_price=fee_price) +def _trade(price, buyer, energy=1, seller=None, fee_price=0.0): + return Trade( + "id", + now(tz=constants.TIME_ZONE), + TraderDetails(seller, ""), + TraderDetails(buyer, ""), + energy, + price, + offer=FakeOffer(price, energy, seller), + fee_price=fee_price, + ) @pytest.fixture(name="area") def fixture_area(): - return FakeArea("parent", - [FakeArea("child1"), - FakeArea("child2", [FakeArea("grandchild1", [FakeArea("-")])]), - FakeArea("child3", [FakeArea("grandchild2")])]) + return FakeArea( + "parent", + [ + FakeArea("child1"), + FakeArea("child2", [FakeArea("grandchild1", [FakeArea("-")])]), + FakeArea("child3", [FakeArea("grandchild2")]), + ], + ) @pytest.fixture(name="markets") @@ -131,18 +150,19 @@ def fixture_markets(): """Example with all equal energy prices""" return ( FakeMarket((_trade(5, "Fridge"), _trade(3, "PV"), _trade(10, "MA 1"))), - FakeMarket((_trade(1, "Storage"), _trade(4, "Fridge"), _trade(6, "Fridge"), - _trade(2, "Fridge"))), - FakeMarket((_trade(11, "MA 3"), _trade(20, "MA 9"), _trade(21, "MA 3"))) + FakeMarket( + (_trade(1, "Storage"), _trade(4, "Fridge"), _trade(6, "Fridge"), _trade(2, "Fridge")) + ), + FakeMarket((_trade(11, "MA 3"), _trade(20, "MA 9"), _trade(21, "MA 3"))), ) @pytest.fixture(name="markets2") def fixture_markets2(): """Example with different energy prices to test weighted averaging""" - return( + return ( FakeMarket((_trade(11, "Fridge", 11), _trade(4, "Storage", 4), _trade(1, "MA 1", 10))), - FakeMarket((_trade(3, "ECar", 1), _trade(9, "Fridge", 3), _trade(3, "Storage", 1))) + FakeMarket((_trade(3, "ECar", 1), _trade(9, "Fridge", 3), _trade(3, "Storage", 1))), ) @@ -150,15 +170,13 @@ def fixture_markets2(): def fixture_grid(): fridge = FakeArea("fridge") pv = FakeArea("pv") - house1 = FakeArea("house1", - children=[fridge, pv]) + house1 = FakeArea("house1", children=[fridge, pv]) house1.past_markets = [FakeMarket((_trade(1, "fridge", 2, "pv"),), "house1")] fridge.parent = house1 pv.parent = house1 e_car = FakeArea("e-car") - house2 = FakeArea("house2", - children=[e_car]) + house2 = FakeArea("house2", children=[e_car]) house2.past_markets = [FakeMarket((_trade(1, "e-car", 1, "ma"),), "house2")] e_car.parent = house2 @@ -175,7 +193,7 @@ def fixture_grid(): "commercial": commercial.uuid, "fridge": fridge.uuid, "pv": pv.uuid, - "e-car": e_car.uuid + "e-car": e_car.uuid, } return grid @@ -185,34 +203,45 @@ def test_energy_bills(grid): epb.spot_market_time_slot_str = grid.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid) m_bills = MarketEnergyBills(should_export_plots=True) - m_bills.update(epb.area_result_dict, epb.flattened_area_core_stats_dict, - epb.spot_market_time_slot_str) + m_bills.update( + epb.area_result_dict, epb.flattened_area_core_stats_dict, epb.spot_market_time_slot_str + ) result = m_bills.bills_results assert result["house2"]["Accumulated Trades"]["bought"] == result["commercial"]["sold"] == 1 - assert result["house2"]["Accumulated Trades"]["spent"] == result["commercial"]["earned"] == \ - 0.01 + assert ( + result["house2"]["Accumulated Trades"]["spent"] == result["commercial"]["earned"] == 0.01 + ) assert result["commercial"]["spent"] == result["commercial"]["bought"] == 0 assert result["fridge"]["bought"] == 2 and isclose(result["fridge"]["spent"], 0.01) assert result["pv"]["sold"] == 2 and isclose(result["pv"]["earned"], 0.01) assert "children" not in result - grid.children[0].past_markets = [FakeMarket((_trade(2, "fridge", 2, "pv"), - _trade(3, "fridge", 1, "ma")), "house1")] - grid.children[1].past_markets = [FakeMarket((_trade(1, "e-car", 4, "ma"), - _trade(1, "e-car", 8, "ma"), - _trade(3, "ma", 5, "e-car")), "house2")] + grid.children[0].past_markets = [ + FakeMarket((_trade(2, "fridge", 2, "pv"), _trade(3, "fridge", 1, "ma")), "house1") + ] + grid.children[1].past_markets = [ + FakeMarket( + ( + _trade(1, "e-car", 4, "ma"), + _trade(1, "e-car", 8, "ma"), + _trade(3, "ma", 5, "e-car"), + ), + "house2", + ) + ] grid.past_markets = [FakeMarket((_trade(2, "house2", 12, "commercial"),), "grid")] epb.spot_market_time_slot_str = grid.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid) - m_bills.update(epb.area_result_dict, epb.flattened_area_core_stats_dict, - epb.spot_market_time_slot_str) + m_bills.update( + epb.area_result_dict, epb.flattened_area_core_stats_dict, epb.spot_market_time_slot_str + ) result = m_bills.bills_results assert result["house2"]["Accumulated Trades"]["bought"] == result["commercial"]["sold"] == 13 - assert result["house2"]["Accumulated Trades"]["spent"] == \ - result["commercial"]["earned"] == \ - 0.03 + assert ( + result["house2"]["Accumulated Trades"]["spent"] == result["commercial"]["earned"] == 0.03 + ) assert result["commercial"]["spent"] == result["commercial"]["bought"] == 0 assert result["fridge"]["bought"] == 5 and isclose(result["fridge"]["spent"], 0.06) assert result["pv"]["sold"] == 4 and isclose(result["pv"]["earned"], 0.03) @@ -224,13 +253,14 @@ def test_energy_bills_last_past_market(grid): epb.spot_market_time_slot_str = grid.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid) m_bills = MarketEnergyBills(should_export_plots=True) - m_bills.update(epb.area_result_dict, epb.flattened_area_core_stats_dict, - epb.spot_market_time_slot_str) + m_bills.update( + epb.area_result_dict, epb.flattened_area_core_stats_dict, epb.spot_market_time_slot_str + ) result = m_bills.bills_results assert result["house2"]["Accumulated Trades"]["bought"] == result["commercial"]["sold"] == 1 - assert result["house2"]["Accumulated Trades"]["spent"] == \ - result["commercial"]["earned"] == \ - 0.01 + assert ( + result["house2"]["Accumulated Trades"]["spent"] == result["commercial"]["earned"] == 0.01 + ) external_trades = result["house2"]["External Trades"] assert external_trades["total_energy"] == external_trades["bought"] - external_trades["sold"] assert external_trades["total_cost"] == external_trades["spent"] - external_trades["earned"] @@ -245,8 +275,9 @@ def test_energy_bills_redis(grid): epb.spot_market_time_slot_str = grid.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid) m_bills = MarketEnergyBills(should_export_plots=True) - m_bills.update(epb.area_result_dict, epb.flattened_area_core_stats_dict, - epb.spot_market_time_slot_str) + m_bills.update( + epb.area_result_dict, epb.flattened_area_core_stats_dict, epb.spot_market_time_slot_str + ) result = m_bills.bills_results result_redis = m_bills.bills_redis_results for house in grid.children: @@ -268,8 +299,9 @@ def test_calculate_raw_energy_bills(grid): assert "children" not in bills[commercial_uuid] house1_uuid = grid.name_uuid_mapping["house1"] assert grid.name_uuid_mapping["pv"] in bills[house1_uuid]["children"] - pv_bills = [v for k, v in bills[house1_uuid]["children"].items() - if k == grid.name_uuid_mapping["pv"]][0] + pv_bills = [ + v for k, v in bills[house1_uuid]["children"].items() if k == grid.name_uuid_mapping["pv"] + ][0] assert pv_bills["sold"] == 2.0 and isclose(pv_bills["earned"], 0.01) assert grid.name_uuid_mapping["fridge"] in bills[house1_uuid]["children"] house2_uuid = grid.name_uuid_mapping["house2"] @@ -277,8 +309,16 @@ def test_calculate_raw_energy_bills(grid): def _compare_bills(bill1, bill2): - key_list = ["spent", "earned", "bought", "sold", "total_energy", "total_cost", - "market_fee", "type"] + key_list = [ + "spent", + "earned", + "bought", + "sold", + "total_energy", + "total_cost", + "market_fee", + "type", + ] for k in key_list: assert bill1[k] == bill2[k] @@ -321,9 +361,7 @@ def fixture_grid2(): grid = FakeArea( "street", children=[house1, house2], - past_markets=[FakeMarket( - (_trade(2, house1.name, 3, house2.name),), "street" - )] + past_markets=[FakeMarket((_trade(2, house1.name, 3, house2.name),), "street")], ) house1.parent = grid house2.parent = grid @@ -335,8 +373,9 @@ def test_energy_bills_finds_mas(grid2): epb.spot_market_time_slot_str = grid2.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid2) m_bills = MarketEnergyBills(should_export_plots=True) - m_bills.update(epb.area_result_dict, epb.flattened_area_core_stats_dict, - epb.spot_market_time_slot_str) + m_bills.update( + epb.area_result_dict, epb.flattened_area_core_stats_dict, epb.spot_market_time_slot_str + ) result = m_bills.bills_results assert result["house1"]["bought"] == result["house2"]["sold"] == 3 @@ -346,8 +385,9 @@ def test_energy_bills_ensure_device_types_are_populated(grid2): epb.spot_market_time_slot_str = grid2.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid2) m_bills = MarketEnergyBills(should_export_plots=True) - m_bills.update(epb.area_result_dict, epb.flattened_area_core_stats_dict, - epb.spot_market_time_slot_str) + m_bills.update( + epb.area_result_dict, epb.flattened_area_core_stats_dict, epb.spot_market_time_slot_str + ) result = m_bills.bills_results assert result["house1"]["type"] == "Area" assert result["house2"]["type"] == "Area" @@ -355,21 +395,31 @@ def test_energy_bills_ensure_device_types_are_populated(grid2): @pytest.fixture(name="grid_fees") def fixture_grid_fees(): - house1 = FakeArea("house1", - children=[FakeArea("testPV")], - past_markets=[FakeMarket([], name="house1", fees=6.0)]) - house2 = FakeArea("house2", - children=[FakeArea("testLoad")], - past_markets=[FakeMarket((_trade(2, "testload", 3, "MA house2", - fee_price=3.0),), name="house2", fees=3.0)]) + house1 = FakeArea( + "house1", + children=[FakeArea("testPV")], + past_markets=[FakeMarket([], name="house1", fees=6.0)], + ) + house2 = FakeArea( + "house2", + children=[FakeArea("testLoad")], + past_markets=[ + FakeMarket( + (_trade(2, "testload", 3, "MA house2", fee_price=3.0),), name="house2", fees=3.0 + ) + ], + ) house1.display_type = "House 1 type" house2.display_type = "House 2 type" grid = FakeArea( "street", children=[house1, house2], - past_markets=[FakeMarket((_trade(2, house2.name, 3, house1.name, - fee_price=1.0),), "street", fees=1.0) - ]) + past_markets=[ + FakeMarket( + (_trade(2, house2.name, 3, house1.name, fee_price=1.0),), "street", fees=1.0 + ) + ], + ) house1.parent = grid house2.parent = grid grid.name_uuid_mapping = { @@ -389,9 +439,13 @@ def test_energy_bills_accumulate_fees(grid_fees): m_bills._update_market_fees(epb.area_result_dict, epb.flattened_area_core_stats_dict) grid_fees.children[0].past_markets = [FakeMarket([], name="house1", fees=2.0)] grid_fees.children[1].past_markets = [] - grid_fees.past_markets = [FakeMarket((_trade(2, grid_fees.children[0].name, 3, - grid_fees.children[0].name, - fee_price=4.0),), "street", fees=4.0)] + grid_fees.past_markets = [ + FakeMarket( + (_trade(2, grid_fees.children[0].name, 3, grid_fees.children[0].name, fee_price=4.0),), + "street", + fees=4.0, + ) + ] epb.spot_market_time_slot_str = grid_fees.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid_fees) m_bills._update_market_fees(epb.area_result_dict, epb.flattened_area_core_stats_dict) @@ -419,17 +473,23 @@ def test_energy_bills_report_correctly_market_fees(grid_fees): epb.spot_market_time_slot_str = grid_fees.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid_fees) m_bills = MarketEnergyBills(should_export_plots=True) - m_bills.update(epb.area_result_dict, epb.flattened_area_core_stats_dict, - epb.spot_market_time_slot_str) + m_bills.update( + epb.area_result_dict, epb.flattened_area_core_stats_dict, epb.spot_market_time_slot_str + ) grid_fees.children[0].past_markets = [FakeMarket([], name="house1", fees=2.0)] grid_fees.children[1].past_markets = [] - grid_fees.past_markets = [FakeMarket((_trade(2, grid_fees.children[0].name, 3, - grid_fees.children[0].name, - fee_price=4.0),), "street", fees=4.0)] + grid_fees.past_markets = [ + FakeMarket( + (_trade(2, grid_fees.children[0].name, 3, grid_fees.children[0].name, fee_price=4.0),), + "street", + fees=4.0, + ) + ] epb.spot_market_time_slot_str = grid_fees.spot_market.time_slot_str epb._populate_core_stats_and_sim_state(grid_fees) - m_bills.update(epb.area_result_dict, epb.flattened_area_core_stats_dict, - epb.spot_market_time_slot_str) + m_bills.update( + epb.area_result_dict, epb.flattened_area_core_stats_dict, epb.spot_market_time_slot_str + ) result = m_bills.bills_results assert result["street"]["house1"]["market_fee"] == 0.04 assert result["street"]["house2"]["market_fee"] == 0.01