def __init__(self, name: str = None, children: List["Area"] = None, uuid: str = None, strategy: BaseStrategy = None, config: SimulationConfig = None, budget_keeper=None, balancing_spot_trade_ratio=ConstSettings.BalancingSettings.SPOT_TRADE_RATIO, event_list=[], grid_fee_percentage: float = None, grid_fee_constant: float = None, external_connection_available: bool = False, throughput: ThroughputParameters = ThroughputParameters() ): validate_area(grid_fee_constant=grid_fee_constant, grid_fee_percentage=grid_fee_percentage) self.balancing_spot_trade_ratio = balancing_spot_trade_ratio self.active = False self.log = TaggedLogWrapper(log, name) self.current_tick = 0 self.__name = name self.throughput = throughput self.uuid = uuid if uuid is not None else str(uuid4()) self.slug = slugify(name, to_lower=True) self.parent = None self.children = AreaChildrenList(self, children) if children is not None\ else AreaChildrenList(self) for child in self.children: child.parent = self if (len(self.children) > 0) and (strategy is not None): raise AreaException("A leaf area can not have children.") self.strategy = strategy self._config = config self.events = Events(event_list, self) self.budget_keeper = budget_keeper if budget_keeper: self.budget_keeper.area = self self._bc = None self._markets = None self.dispatcher = DispatcherFactory(self)() self._set_grid_fees(grid_fee_constant, grid_fee_percentage) self.display_type = "Area" if self.strategy is None else self.strategy.__class__.__name__ self._markets = AreaMarkets(self.log) self.stats = AreaStats(self._markets, self) log.debug("External connection %s for area %s", external_connection_available, self.name) self.redis_ext_conn = RedisMarketExternalConnection(self) \ if external_connection_available and self.strategy is None else None self.should_update_child_strategies = False self.external_connection_available = external_connection_available
def __init__(self, name: str = None, children: List["Area"] = None, strategy: BaseStrategy = None, appliance: BaseAppliance = None, config: SimulationConfig = None, budget_keeper=None, balancing_spot_trade_ratio=ConstSettings.BalancingSettings. SPOT_TRADE_RATIO): self.balancing_spot_trade_ratio = balancing_spot_trade_ratio self.active = False self.log = TaggedLogWrapper(log, name) self.current_tick = 0 self.name = name self.slug = slugify(name, to_lower=True) self.area_id = Area._area_id_counter Area._area_id_counter += 1 self.parent = None self.children = children if children is not None else [] for child in self.children: child.parent = self self.strategy = strategy self.appliance = appliance self._config = config self.budget_keeper = budget_keeper if budget_keeper: self.budget_keeper.area = self self._bc = None # type: BlockChainInterface self._markets = AreaMarkets(self.log) self.stats = AreaStats(self._markets) self.dispatcher = AreaDispatcher(self)
def setUp(self): ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True DeviceRegistry.REGISTRY = { "H1 General Load": (33, 35), "H2 General Load": (33, 35), "H1 Storage1": (23, 25), "H1 Storage2": (23, 25), } self.appliance = MagicMock(spec=SimpleAppliance) self.strategy = MagicMock(spec=StorageStrategy) self.config = MagicMock(spec=SimulationConfig) self.config.slot_length = duration(minutes=15) self.config.tick_length = duration(seconds=15) self.config.start_date = today(tz=TIME_ZONE) self.config.sim_duration = duration(days=1) self.area = Area("test_area", None, None, self.strategy, self.appliance, self.config, None, transfer_fee_pct=1) self.area.parent = self.area self.area.children = [self.area] self.area.transfer_fee_pct = 1 self.dispatcher = AreaDispatcher(self.area) self.stats = AreaStats(self.area._markets)
def __init__(self, name: str = None, children: List["Area"] = None, uuid: str = None, strategy: BaseStrategy = None, appliance: BaseAppliance = None, config: SimulationConfig = None, budget_keeper=None, balancing_spot_trade_ratio=ConstSettings.BalancingSettings. SPOT_TRADE_RATIO, event_list=[], grid_fee_percentage: float = None, transfer_fee_const: float = None, external_connection_available=False): validate_area(grid_fee_percentage=grid_fee_percentage) self.balancing_spot_trade_ratio = balancing_spot_trade_ratio self.active = False self.log = TaggedLogWrapper(log, name) self.current_tick = 0 self.name = name self.uuid = uuid if uuid is not None else str(uuid4()) self.slug = slugify(name, to_lower=True) self.parent = None self.children = children if children is not None else [] for child in self.children: child.parent = self if (len(self.children) > 0) and (strategy is not None): raise AreaException("A leaf area can not have children.") self.strategy = strategy self.appliance = appliance self._config = config self.events = Events(event_list, self) self.budget_keeper = budget_keeper if budget_keeper: self.budget_keeper.area = self self._bc = None self._markets = None self.dispatcher = DispatcherFactory(self)() self.grid_fee_percentage = grid_fee_percentage self.transfer_fee_const = transfer_fee_const self.display_type = "Area" if self.strategy is None else self.strategy.__class__.__name__ self._markets = AreaMarkets(self.log) self.stats = AreaStats(self._markets) self.redis_ext_conn = RedisAreaExternalConnection(self) \ if external_connection_available is True else None
def setUp(self): ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True DeviceRegistry.REGISTRY = { "H1 General Load": (33, 35), "H2 General Load": (33, 35), "H1 Storage1": (23, 25), "H1 Storage2": (23, 25), } self.strategy = MagicMock(spec=StorageStrategy) self.config = MagicMock(spec=SimulationConfig) self.config.slot_length = duration(minutes=15) self.config.tick_length = duration(seconds=15) self.config.ticks_per_slot = int(self.config.slot_length.seconds / self.config.tick_length.seconds) self.config.start_date = today(tz=constants.TIME_ZONE) GlobalConfig.sim_duration = duration(days=1) self.config.sim_duration = duration(days=1) self.config.grid_fee_type = 1 self.config.end_date = self.config.start_date + self.config.sim_duration self.area = Area("test_area", None, None, self.strategy, self.config, None, grid_fee_percentage=1) self.area_child = Area("test_area_c", None, None, self.strategy, self.config, None, grid_fee_percentage=1) self.area_child.parent = self.area self.area.children = [self.area_child] self.area.grid_fee_percentage = 1 self.dispatcher = AreaDispatcher(self.area) self.stats = AreaStats(self.area._markets, self.area)
def __init__(self, name: str = None, children: List["Area"] = None, uuid: str = None, strategy: BaseStrategy = None, appliance: BaseAppliance = None, config: SimulationConfig = None, budget_keeper=None, balancing_spot_trade_ratio=ConstSettings.BalancingSettings. SPOT_TRADE_RATIO, event_list=[], grid_fee_percentage: float = None, transfer_fee_const: float = None, external_connection_available: bool = False, baseline_peak_energy_import_kWh: float = None, baseline_peak_energy_export_kWh: float = None, import_capacity_kVA: float = None, export_capacity_kVA: float = None): validate_area( grid_fee_constant=transfer_fee_const, grid_fee_percentage=grid_fee_percentage, baseline_peak_energy_import_kWh=baseline_peak_energy_import_kWh, baseline_peak_energy_export_kWh=baseline_peak_energy_export_kWh, import_capacity_kVA=import_capacity_kVA, export_capacity_kVA=export_capacity_kVA) self.balancing_spot_trade_ratio = balancing_spot_trade_ratio self.active = False self.log = TaggedLogWrapper(log, name) self.current_tick = 0 self.name = name self.baseline_peak_energy_import_kWh = baseline_peak_energy_import_kWh self.baseline_peak_energy_export_kWh = baseline_peak_energy_export_kWh self.uuid = uuid if uuid is not None else str(uuid4()) self.slug = slugify(name, to_lower=True) self.parent = None self.children = children if children is not None else [] for child in self.children: child.parent = self if (len(self.children) > 0) and (strategy is not None): raise AreaException("A leaf area can not have children.") self.strategy = strategy self.appliance = appliance self._config = config self.events = Events(event_list, self) self.budget_keeper = budget_keeper if budget_keeper: self.budget_keeper.area = self self._bc = None self._markets = None self.dispatcher = DispatcherFactory(self)() self._set_grid_fees(transfer_fee_const, grid_fee_percentage) self._convert_area_throughput_kva_to_kwh(import_capacity_kVA, export_capacity_kVA) self.display_type = "Area" if self.strategy is None else self.strategy.__class__.__name__ self._markets = AreaMarkets(self.log) self.endpoint_stats = {} self.stats = AreaStats(self._markets) log.debug( f"External connection {external_connection_available} for area {self.name}" ) self.redis_ext_conn = RedisMarketExternalConnection(self) \ if external_connection_available is True else None
class Area: reconfig_parameters = ('grid_fee_percentage', 'transfer_fee_const', 'baseline_peak_energy_import_kWh', 'baseline_peak_energy_export_kWh', 'import_capacity_kVA', 'export_capacity_kVA') def __init__(self, name: str = None, children: List["Area"] = None, uuid: str = None, strategy: BaseStrategy = None, appliance: BaseAppliance = None, config: SimulationConfig = None, budget_keeper=None, balancing_spot_trade_ratio=ConstSettings.BalancingSettings. SPOT_TRADE_RATIO, event_list=[], grid_fee_percentage: float = None, transfer_fee_const: float = None, external_connection_available: bool = False, baseline_peak_energy_import_kWh: float = None, baseline_peak_energy_export_kWh: float = None, import_capacity_kVA: float = None, export_capacity_kVA: float = None): validate_area( grid_fee_constant=transfer_fee_const, grid_fee_percentage=grid_fee_percentage, baseline_peak_energy_import_kWh=baseline_peak_energy_import_kWh, baseline_peak_energy_export_kWh=baseline_peak_energy_export_kWh, import_capacity_kVA=import_capacity_kVA, export_capacity_kVA=export_capacity_kVA) self.balancing_spot_trade_ratio = balancing_spot_trade_ratio self.active = False self.log = TaggedLogWrapper(log, name) self.current_tick = 0 self.name = name self.baseline_peak_energy_import_kWh = baseline_peak_energy_import_kWh self.baseline_peak_energy_export_kWh = baseline_peak_energy_export_kWh self.uuid = uuid if uuid is not None else str(uuid4()) self.slug = slugify(name, to_lower=True) self.parent = None self.children = children if children is not None else [] for child in self.children: child.parent = self if (len(self.children) > 0) and (strategy is not None): raise AreaException("A leaf area can not have children.") self.strategy = strategy self.appliance = appliance self._config = config self.events = Events(event_list, self) self.budget_keeper = budget_keeper if budget_keeper: self.budget_keeper.area = self self._bc = None self._markets = None self.dispatcher = DispatcherFactory(self)() self._set_grid_fees(transfer_fee_const, grid_fee_percentage) self._convert_area_throughput_kva_to_kwh(import_capacity_kVA, export_capacity_kVA) self.display_type = "Area" if self.strategy is None else self.strategy.__class__.__name__ self._markets = AreaMarkets(self.log) self.endpoint_stats = {} self.stats = AreaStats(self._markets) log.debug( f"External connection {external_connection_available} for area {self.name}" ) self.redis_ext_conn = RedisMarketExternalConnection(self) \ if external_connection_available is True else None def area_reconfigure_event(self, **kwargs): if self.strategy is not None: self.strategy.area_reconfigure_event(**kwargs) return if key_in_dict_and_not_none(kwargs, 'transfer_fee_const') or \ key_in_dict_and_not_none(kwargs, 'grid_fee_percentage'): transfer_fee_const = kwargs["transfer_fee_const"] \ if key_in_dict_and_not_none(kwargs, 'transfer_fee_const') else None grid_fee_percentage = kwargs["grid_fee_percentage"] \ if key_in_dict_and_not_none(kwargs, 'grid_fee_percentage') else None validate_area(grid_fee_percentage=grid_fee_percentage, grid_fee_constant=transfer_fee_const) self._set_grid_fees(transfer_fee_const, grid_fee_percentage) if key_in_dict_and_not_none(kwargs, 'baseline_peak_energy_import_kWh'): self.baseline_peak_energy_import_kWh = kwargs[ 'baseline_peak_energy_import_kWh'] validate_area(baseline_peak_energy_import_kWh=self. baseline_peak_energy_import_kWh) if key_in_dict_and_not_none(kwargs, 'baseline_peak_energy_export_kWh'): self.baseline_peak_energy_export_kWh = kwargs[ 'baseline_peak_energy_export_kWh'] validate_area(baseline_peak_energy_export_kWh=self. baseline_peak_energy_export_kWh) if key_in_dict_and_not_none(kwargs, 'import_capacity_kVA') or \ key_in_dict_and_not_none(kwargs, 'export_capacity_kVA'): import_capacity_kVA = kwargs["import_capacity_kVA"] \ if key_in_dict_and_not_none(kwargs, 'import_capacity_kVA') else None export_capacity_kVA = kwargs["export_capacity_kVA"] \ if key_in_dict_and_not_none(kwargs, 'export_capacity_kVA') else None validate_area(import_capacity_kVA=import_capacity_kVA, export_capacity_kVA=export_capacity_kVA) self._convert_area_throughput_kva_to_kwh(import_capacity_kVA, export_capacity_kVA) def _set_grid_fees(self, transfer_fee_const, grid_fee_percentage): grid_fee_type = self.config.grid_fee_type \ if self.config is not None \ else ConstSettings.IAASettings.GRID_FEE_TYPE if grid_fee_type == 1: grid_fee_percentage = None elif grid_fee_type == 2: transfer_fee_const = None self.transfer_fee_const = transfer_fee_const self.grid_fee_percentage = grid_fee_percentage def _convert_area_throughput_kva_to_kwh(self, import_capacity_kVA, export_capacity_kVA): self.import_capacity_kWh = \ import_capacity_kVA * self.config.slot_length.total_minutes() / 60.0 \ if import_capacity_kVA is not None else 0. self.export_capacity_kWh = \ export_capacity_kVA * self.config.slot_length.total_minutes() / 60.0 \ if export_capacity_kVA is not None else 0. def set_events(self, event_list): self.events = Events(event_list, self) def activate(self, bc=None, current_tick=None): if current_tick is not None: self.current_tick = current_tick if bc: self._bc = bc for attr, kind in [(self.strategy, 'Strategy'), (self.appliance, 'Appliance')]: if attr: if self.parent: attr.area = self.parent attr.owner = self else: raise AreaException("{kind} {attr.__class__.__name__} " "on area {s} without parent!".format( kind=kind, attr=attr, s=self)) if self.budget_keeper: self.budget_keeper.activate() if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0: self.grid_fee_percentage = 0 # Cycle markets without triggering it's own event chain. self._cycle_markets(_trigger_event=False) if not self.strategy and self.parent is not None: self.log.debug("No strategy. Using inter area agent.") self.log.debug('Activating area') self.active = True self.dispatcher.broadcast_activate() if self.redis_ext_conn is not None: self.redis_ext_conn.sub_to_area_event() def deactivate(self): self._cycle_markets(deactivate=True) if self.redis_ext_conn is not None: self.redis_ext_conn.deactivate() if self.strategy: self.strategy.deactivate() def _cycle_markets(self, _trigger_event=True, _market_cycle=False, deactivate=False): """ Remove markets for old time slots, add markets for new slots. Trigger `MARKET_CYCLE` event to allow child markets to also cycle. It's important for this to happen from top to bottom of the `Area` tree in order for the `InterAreaAgent`s to be connected correctly `_trigger_event` is used internally to avoid multiple event chains during initial area activation. """ self.events.update_events(self.now) if not self.children: # Since children trade in markets we only need to populate them if there are any return if self.budget_keeper and _market_cycle: self.budget_keeper.process_market_cycle() self.log.debug("Cycling markets") self._markets.rotate_markets(self.now, self.stats, self.dispatcher) self.dispatcher._delete_past_agents(self.dispatcher._inter_area_agents) if deactivate: return # Clear `current_market` cache self.__dict__.pop('current_market', None) # area_market_stats have to updated when cycling market of each area: self.stats.update_area_market_stats() # Markets range from one slot to market_count into the future changed = self._markets.create_future_markets(self.now, True, self) if ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET and \ len(DeviceRegistry.REGISTRY.keys()) != 0: changed_balancing_market = self._markets.create_future_markets( self.now, False, self) else: changed_balancing_market = None # Force market cycle event in case this is the first market slot if (changed or len(self._markets.past_markets.keys()) == 0) and _trigger_event: self.dispatcher.broadcast_market_cycle() # Force balancing_market cycle event in case this is the first market slot if (changed_balancing_market or len(self._markets.past_balancing_markets.keys()) == 0) \ and _trigger_event and ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET: self.dispatcher.broadcast_balancing_market_cycle() if self.redis_ext_conn is not None: self.redis_ext_conn.event_market_cycle() def tick(self): if ConstSettings.IAASettings.MARKET_TYPE == 2 or \ ConstSettings.IAASettings.MARKET_TYPE == 3: if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS: self.dispatcher.publish_market_clearing() else: for market in self.all_markets: market.match_offers_bids() self.events.update_events(self.now) self.current_tick += 1 if self._markets: for market in self._markets.markets.values(): market.update_clock(self.current_tick_in_slot) def tick_and_dispatch(self): if d3a.constants.DISPATCH_EVENTS_BOTTOM_TO_TOP: self.dispatcher.broadcast_tick() self.tick() else: self.tick() self.dispatcher.broadcast_tick() def __repr__(self): return "<Area '{s.name}' markets: {markets}>".format( s=self, markets=[ t.format(TIME_FORMAT) for t in self._markets.markets.keys() ]) @property def current_slot(self): return self.current_tick // self.config.ticks_per_slot @property def current_tick_in_slot(self): return self.current_tick % self.config.ticks_per_slot @property def config(self): if self._config: return self._config if self.parent: return self.parent.config return GlobalConfig @property def bc(self): if self._bc is not None: return self._bc if self.parent: return self.parent.bc return None @cached_property def child_by_slug(self): slug_map = {} areas = [self] while areas: for area in list(areas): slug_map[area.slug] = area areas.remove(area) areas.extend(area.children) return slug_map @property def now(self) -> DateTime: """ Return the 'current time' as a `DateTime` object. Can be overridden in subclasses to change the meaning of 'now'. In this default implementation 'current time' is defined by the number of ticks that have passed. """ return self.config.start_date.add( seconds=self.config.tick_length.seconds * self.current_tick) @property def all_markets(self): return [m for m in self._markets.markets.values() if m.in_sim_duration] @property def past_markets(self): return list(self._markets.past_markets.values()) def get_market(self, timeslot): return self._markets.markets[timeslot] def get_past_market(self, timeslot): return self._markets.past_markets[timeslot] def get_balancing_market(self, timeslot): return self._markets.balancing_markets[timeslot] @property def balancing_markets(self): return list(self._markets.balancing_markets.values()) @property def past_balancing_markets(self): return list(self._markets.past_balancing_markets.values()) @property def market_with_most_expensive_offer(self): # In case of a tie, max returns the first market occurrence in order to # satisfy the most recent market slot return max(self.all_markets, key=lambda m: m.sorted_offers[0].energy_rate) @property def next_market(self): """Returns the 'current' market (i.e. the one currently 'running')""" try: return list(self._markets.markets.values())[0] except IndexError: return None @property def current_market(self): """Returns the 'most recent past market' market (i.e. the one that has been finished last)""" try: return list(self._markets.past_markets.values())[-1] except IndexError: return None @property def current_balancing_market(self): """Returns the 'current' balancing market (i.e. the one currently 'running')""" try: return list(self._markets.past_balancing_markets.values())[-1] except IndexError: return None def get_future_market_from_id(self, _id): return self._markets.indexed_future_markets.get(_id, None) @property def last_past_market(self): try: return list(self._markets.past_markets.values())[-1] except IndexError: return None @cached_property def available_triggers(self): triggers = [] if isinstance(self.strategy, TriggerMixin): triggers.extend(self.strategy.available_triggers) if isinstance(self.appliance, TriggerMixin): triggers.extend(self.appliance.available_triggers) return {t.name: t for t in triggers} def _fire_trigger(self, trigger_name, **params): for target in (self.strategy, self.appliance): if isinstance(target, TriggerMixin): for trigger in target.available_triggers: if trigger.name == trigger_name: return target.fire_trigger(trigger_name, **params) def update_config(self, **kwargs): if not self.config: return self.config.update_config_parameters(**kwargs) if self.strategy: self.strategy.read_config_event() for child in self.children: child.update_config(**kwargs)
class Area: def __init__(self, name: str = None, children: List["Area"] = None, uuid: str = None, strategy: BaseStrategy = None, config: SimulationConfig = None, budget_keeper=None, balancing_spot_trade_ratio=ConstSettings.BalancingSettings.SPOT_TRADE_RATIO, event_list=[], grid_fee_percentage: float = None, grid_fee_constant: float = None, external_connection_available: bool = False, throughput: ThroughputParameters = ThroughputParameters() ): validate_area(grid_fee_constant=grid_fee_constant, grid_fee_percentage=grid_fee_percentage) self.balancing_spot_trade_ratio = balancing_spot_trade_ratio self.active = False self.log = TaggedLogWrapper(log, name) self.current_tick = 0 self.__name = name self.throughput = throughput self.uuid = uuid if uuid is not None else str(uuid4()) self.slug = slugify(name, to_lower=True) self.parent = None self.children = AreaChildrenList(self, children) if children is not None\ else AreaChildrenList(self) for child in self.children: child.parent = self if (len(self.children) > 0) and (strategy is not None): raise AreaException("A leaf area can not have children.") self.strategy = strategy self._config = config self.events = Events(event_list, self) self.budget_keeper = budget_keeper if budget_keeper: self.budget_keeper.area = self self._bc = None self._markets = None self.dispatcher = DispatcherFactory(self)() self._set_grid_fees(grid_fee_constant, grid_fee_percentage) self.display_type = "Area" if self.strategy is None else self.strategy.__class__.__name__ self._markets = AreaMarkets(self.log) self.stats = AreaStats(self._markets, self) log.debug("External connection %s for area %s", external_connection_available, self.name) self.redis_ext_conn = RedisMarketExternalConnection(self) \ if external_connection_available and self.strategy is None else None self.should_update_child_strategies = False self.external_connection_available = external_connection_available @property def name(self): return self.__name @name.setter def name(self, new_name): if check_area_name_exists_in_parent_area(self.parent, new_name): raise AreaException("Area name should be unique inside the same Parent Area") self.__name = new_name def get_state(self): state = {} if self.strategy is not None: state = self.strategy.get_state() state.update(**{ "current_tick": self.current_tick, "area_stats": self.stats.get_state() }) return state def restore_state(self, saved_state): self.current_tick = saved_state["current_tick"] self.stats.restore_state(saved_state["area_stats"]) if self.strategy is not None: self.strategy.restore_state(saved_state) def area_reconfigure_event(self, **kwargs): """Reconfigure the device properties at runtime using the provided arguments.""" if self.strategy is not None: self.strategy.area_reconfigure_event(**kwargs) return True grid_fee_constant = ( kwargs["grid_fee_constant"] if key_in_dict_and_not_none(kwargs, "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) baseline_peak_energy_import_kWh = ( kwargs["baseline_peak_energy_import_kWh"] if key_in_dict_and_not_none( kwargs, "baseline_peak_energy_import_kWh") else self.throughput.baseline_peak_energy_import_kWh) baseline_peak_energy_export_kWh = ( kwargs["baseline_peak_energy_export_kWh"] if key_in_dict_and_not_none( kwargs, "baseline_peak_energy_export_kWh") else self.throughput.baseline_peak_energy_export_kWh) import_capacity_kVA = ( kwargs["import_capacity_kVA"] if key_in_dict_and_not_none(kwargs, "import_capacity_kVA") else self.throughput.import_capacity_kVA) export_capacity_kVA = ( kwargs["export_capacity_kVA"] if key_in_dict_and_not_none(kwargs, "export_capacity_kVA") else self.throughput.import_capacity_kVA) try: validate_area(grid_fee_constant=grid_fee_constant, grid_fee_percentage=grid_fee_percentage) throughput = ThroughputParameters( baseline_peak_energy_import_kWh=baseline_peak_energy_import_kWh, baseline_peak_energy_export_kWh=baseline_peak_energy_export_kWh, import_capacity_kVA=import_capacity_kVA, export_capacity_kVA=export_capacity_kVA ) except Exception as ex: log.error(ex) return self._set_grid_fees(grid_fee_constant, grid_fee_percentage) self.throughput = throughput self._update_descendants_strategy_prices() def _update_descendants_strategy_prices(self): try: if self.strategy is not None: self.strategy.event_activate_price() for child in self.children: child._update_descendants_strategy_prices() except Exception: log.exception("area._update_descendants_strategy_prices failed.") return 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 \ else ConstSettings.IAASettings.GRID_FEE_TYPE if grid_fee_type == 1: grid_fee_percentage = None elif grid_fee_type == 2: grid_fee_const = None self.grid_fee_constant = grid_fee_const self.grid_fee_percentage = grid_fee_percentage def get_path_to_root_fees(self): if self.parent is not None: grid_fee_constant = self.grid_fee_constant if self.grid_fee_constant else 0 return grid_fee_constant + self.parent.get_path_to_root_fees() return self.grid_fee_constant if self.grid_fee_constant else 0 def get_grid_fee(self): grid_fee_type = self.config.grid_fee_type \ if self.config is not None \ else ConstSettings.IAASettings.GRID_FEE_TYPE return self.grid_fee_constant if grid_fee_type == 1 else self.grid_fee_percentage def set_events(self, event_list): self.events = Events(event_list, self) def activate(self, bc=None, current_tick=None, simulation_id=None): if current_tick is not None: self.current_tick = current_tick self._bc = blockchain_interface_factory(bc, self.uuid, simulation_id) if self.strategy: if self.parent: self.strategy.area = self.parent self.strategy.owner = self else: raise AreaException( f"Strategy {self.strategy.__class__.__name__} on area {self} without parent!" ) if self.budget_keeper: self.budget_keeper.activate() if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0: self._set_grid_fees(0, 0) # Cycle markets without triggering it's own event chain. self.cycle_markets(_trigger_event=False) if not self.strategy and self.parent is not None: self.log.debug("No strategy. Using inter area agent.") self.log.debug("Activating area") self.active = True self.dispatcher.broadcast_activate(bc=bc, current_tick=self.current_tick, simulation_id=simulation_id) if self.redis_ext_conn is not None: self.redis_ext_conn.sub_to_external_channels() def deactivate(self): self.cycle_markets(deactivate=True) if self.redis_ext_conn is not None: self.redis_ext_conn.deactivate() if self.strategy: self.strategy.deactivate() def cycle_markets(self, _trigger_event=True, _market_cycle=False, deactivate=False): """ Remove markets for old time slots, add markets for new slots. Trigger `MARKET_CYCLE` event to allow child markets to also cycle. It's important for this to happen from top to bottom of the `Area` tree in order for the `InterAreaAgent`s to be connected correctly `_trigger_event` is used internally to avoid multiple event chains during initial area activation. """ current_tick_in_slot = int(self.current_tick % self.config.ticks_per_slot) tick_at_the_slot_start = self.current_tick - current_tick_in_slot if tick_at_the_slot_start == 0: now_value = self.now else: datetime_at_the_slot_start = self.config.start_date.add( seconds=self.config.tick_length.seconds * tick_at_the_slot_start ) now_value = datetime_at_the_slot_start self.events.update_events(now_value) if not self.children: # Since children trade in markets we only need to populate them if there are any return if self.budget_keeper and _market_cycle: self.budget_keeper.process_market_cycle() self.log.debug("Cycling markets") self._markets.rotate_markets(now_value) self.dispatcher._delete_past_agents(self.dispatcher._inter_area_agents) # area_market_stats have to updated when cycling market of each area: self.stats.update_area_market_stats() if deactivate: return if self.should_update_child_strategies is True: self._update_descendants_strategy_prices() self.should_update_child_strategies = False # Clear `current_market` cache self.__dict__.pop("current_market", None) # Markets range from one slot to market_count into the future changed = self._markets.create_future_markets(now_value, True, self) if ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET and \ len(DeviceRegistry.REGISTRY.keys()) != 0: changed_balancing_market = self._markets.create_future_markets(now_value, False, self) else: changed_balancing_market = None # Force market cycle event in case this is the first market slot if (changed or len(self._markets.past_markets.keys()) == 0) and _trigger_event: self.dispatcher.broadcast_market_cycle() # Force balancing_market cycle event in case this is the first market slot if (changed_balancing_market or len(self._markets.past_balancing_markets.keys()) == 0) \ and _trigger_event and ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET: self.dispatcher.broadcast_balancing_market_cycle() def publish_market_cycle_to_external_clients(self): if self.strategy and isinstance(self.strategy, ExternalMixin): self.strategy.publish_market_cycle() elif not self.strategy and self.external_connection_available: self.redis_ext_conn.publish_market_cycle() for child in self.children: child.publish_market_cycle_to_external_clients() def _consume_commands_from_aggregator(self): if self.redis_ext_conn is not None and self.redis_ext_conn.is_aggregator_controlled: (self.redis_ext_conn.aggregator. consume_all_area_commands(self.uuid, self.redis_ext_conn.trigger_aggregator_commands)) elif (self.strategy and getattr(self.strategy, "is_aggregator_controlled", False)): (self.strategy.redis.aggregator. consume_all_area_commands(self.uuid, self.strategy.trigger_aggregator_commands)) def tick(self): """Tick event handler. Invoke aggregator commands consumer, publishes market clearing, updates events, updates cached myco matcher markets and match trades recommendations. """ self._consume_commands_from_aggregator() if (ConstSettings.IAASettings.MARKET_TYPE != SpotMarketTypeEnum.ONE_SIDED.value and not self.strategy): if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS: self.dispatcher.publish_market_clearing() else: self._update_myco_matcher() bid_offer_matcher.match_recommendations() self.events.update_events(self.now) def _update_myco_matcher(self) -> None: """Update the markets cache that the myco matcher will request""" bid_offer_matcher.update_area_uuid_markets_mapping( area_uuid_markets_mapping={ self.uuid: {"markets": self.all_markets, "current_time": self.now}}) def update_area_current_tick(self): self.current_tick += 1 if self._markets: for market in self._markets.markets.values(): market.update_clock(self.current_tick_in_slot) for child in self.children: child.update_area_current_tick() def tick_and_dispatch(self): """Invoke tick handler and broadcast the event to children.""" if d3a.constants.DISPATCH_EVENTS_BOTTOM_TO_TOP: self.dispatcher.broadcast_tick() self.tick() else: self.tick() self.dispatcher.broadcast_tick() def __repr__(self): return "<Area '{s.name}' markets: {markets}>".format( s=self, markets=[t.format(d3a.constants.TIME_FORMAT) for t in self._markets.markets.keys()] ) @property def current_slot(self): return self.current_tick // self.config.ticks_per_slot @property def current_tick_in_slot(self): return self.current_tick % self.config.ticks_per_slot @property def config(self): if self._config: return self._config if self.parent: return self.parent.config return GlobalConfig @property def bc(self): if self._bc is not None: return self._bc return None @cached_property def child_by_slug(self): slug_map = {} areas = [self] while areas: for area in list(areas): slug_map[area.slug] = area areas.remove(area) areas.extend(area.children) return slug_map @property def now(self) -> DateTime: """ Return the "current time" as a "DateTime" object. Can be overridden in subclasses to change the meaning of "now". In this default implementation "current time" is defined by the number of ticks that have passed. """ return self.config.start_date.add( seconds=self.config.tick_length.seconds * self.current_tick ) @property def all_markets(self): return [m for m in self._markets.markets.values() if m.in_sim_duration] @property def past_markets(self): return list(self._markets.past_markets.values()) def get_market(self, timeslot): return self._markets.markets[timeslot] def get_past_market(self, timeslot): return self._markets.past_markets[timeslot] def get_balancing_market(self, timeslot): return self._markets.balancing_markets[timeslot] @property def balancing_markets(self): return list(self._markets.balancing_markets.values()) @property def past_balancing_markets(self): return list(self._markets.past_balancing_markets.values()) @property def market_with_most_expensive_offer(self): # In case of a tie, max returns the first market occurrence in order to # satisfy the most recent market slot return max(self.all_markets, key=lambda m: m.sorted_offers[0].energy_rate) @property def next_market(self): """Returns the "current" market (i.e. the one currently "running")""" try: return list(self._markets.markets.values())[0] except IndexError: return None @property def current_market(self): """Returns the "most recent past market" market (i.e. the one that has been finished last)""" try: return list(self._markets.past_markets.values())[-1] except IndexError: return None @property def current_balancing_market(self): """Returns the "current" balancing market (i.e. the one currently "running")""" try: return list(self._markets.past_balancing_markets.values())[-1] except IndexError: return None def get_future_market_from_id(self, _id): return self._markets.indexed_future_markets.get(_id, None) @property def last_past_market(self): try: return list(self._markets.past_markets.values())[-1] except IndexError: return None @cached_property def available_triggers(self): triggers = [] if isinstance(self.strategy, TriggerMixin): triggers.extend(self.strategy.available_triggers) return {t.name: t for t in triggers} def update_config(self, **kwargs): if not self.config: return self.config.update_config_parameters(**kwargs) if self.strategy: self.strategy.read_config_event() for child in self.children: child.update_config(**kwargs)