From e9d9b73153f3803a178c54c5037370ee15f33731 Mon Sep 17 00:00:00 2001 From: spyrostz Date: Wed, 7 Aug 2024 14:07:04 +0200 Subject: [PATCH] RESEARCH-112: Added optional linear_pricing parameter to all strategies, that is overriding fit_to_limit, to harmonize code and documentation. Fixed .pylintrc to remove deprecated disable flags. Fixed pylint warnings. --- .pylintrc | 75 +-- src/gsy_e/models/strategy/__init__.py | 423 +++++++++++------ .../strategy/external_strategies/load.py | 429 +++++++++++------- .../models/strategy/external_strategies/pv.py | 319 ++++++++----- src/gsy_e/models/strategy/load_hours.py | 243 ++++++---- src/gsy_e/models/strategy/predefined_load.py | 72 +-- src/gsy_e/models/strategy/predefined_pv.py | 107 +++-- src/gsy_e/models/strategy/predefined_wind.py | 41 +- src/gsy_e/models/strategy/pv.py | 159 ++++--- src/gsy_e/models/strategy/smart_meter.py | 195 +++++--- src/gsy_e/models/strategy/storage.py | 358 +++++++++------ 11 files changed, 1489 insertions(+), 932 deletions(-) diff --git a/.pylintrc b/.pylintrc index dbb2ca8038..a3d0730caa 100644 --- a/.pylintrc +++ b/.pylintrc @@ -66,16 +66,6 @@ confidence= # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=invalid-name, - print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, raw-checker-failed, bad-inline-option, locally-disabled, @@ -84,67 +74,6 @@ disable=invalid-name, useless-suppression, deprecated-pragma, use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, missing-module-docstring, too-many-ancestors, duplicate-code @@ -607,5 +536,5 @@ min-public-methods=0 # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/src/gsy_e/models/strategy/__init__.py b/src/gsy_e/models/strategy/__init__.py index c71df8da93..f4e430eaba 100644 --- a/src/gsy_e/models/strategy/__init__.py +++ b/src/gsy_e/models/strategy/__init__.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import json import logging import sys @@ -60,6 +61,7 @@ @dataclass class AcceptOfferParameters: """Parameters for the accept_offer MarketStrategyConnectionAdapter methods""" + market: Union["OneSidedMarket", str] offer: Offer buyer: TraderDetails @@ -68,22 +70,27 @@ class AcceptOfferParameters: def to_dict(self) -> dict: """Convert dataclass to dict in order to be able to send these arguments via Redis.""" - return {"offer_or_id": self.offer.to_json_string(), - "buyer": self.buyer.serializable_dict(), - "energy": self.energy, - "trade_bid_info": self.trade_bid_info.serializable_dict()} + return { + "offer_or_id": self.offer.to_json_string(), + "buyer": self.buyer.serializable_dict(), + "energy": self.energy, + "trade_bid_info": self.trade_bid_info.serializable_dict(), + } def accept_offer_using_market_object(self) -> Trade: """Calls accept offer on the market object that is contained in the dataclass, - using the arguments from the other dataclass members""" + using the arguments from the other dataclass members""" return self.market.accept_offer( - offer_or_id=self.offer, buyer=self.buyer, + offer_or_id=self.offer, + buyer=self.buyer, energy=self.energy, - trade_bid_info=self.trade_bid_info) + trade_bid_info=self.trade_bid_info, + ) class _TradeLookerUpper: """Find trades that concern the strategy from the selected market""" + def __init__(self, owner_name: str): self.owner_name = owner_name @@ -94,8 +101,9 @@ def __getitem__(self, market: MarketBase) -> Generator[Trade, None, None]: yield trade -def market_strategy_connection_adapter_factory() -> Union["MarketStrategyConnectionAdapter", - "MarketStrategyConnectionRedisAdapter"]: +def market_strategy_connection_adapter_factory() -> ( + Union["MarketStrategyConnectionAdapter", "MarketStrategyConnectionRedisAdapter"] +): """ Return an object of either MarketStrategyConnectionRedisAdapter or MarketStrategyConnectionAdapter, depending on whether the flag EVENT_DISPATCHING_VIA_REDIS is @@ -114,6 +122,7 @@ class MarketStrategyConnectionRedisAdapter: the class methods. Useful for markets that are run in a remote service, different from the strategy. """ + def __init__(self): self.redis = BlockingCommunicator() self._trade_buffer: Optional[Trade] = None @@ -132,8 +141,7 @@ def offer(self, market: Union["OneSidedMarket", str], offer_args: dict) -> Offer """ market_id = market.id if not isinstance(market, str) else market - self._send_events_to_market("OFFER", market_id, offer_args, - self._redis_offer_response) + self._send_events_to_market("OFFER", market_id, offer_args, self._redis_offer_response) offer = self._offer_buffer assert offer is not None self._offer_buffer = None @@ -141,12 +149,18 @@ def offer(self, market: Union["OneSidedMarket", str], offer_args: dict) -> Offer def accept_offer(self, offer_parameters: AcceptOfferParameters) -> Trade: """Accept an offer on a market via Redis.""" - market_id = (offer_parameters.market.id - if not isinstance(offer_parameters.market, str) - else offer_parameters.market) + market_id = ( + offer_parameters.market.id + if not isinstance(offer_parameters.market, str) + else offer_parameters.market + ) - self._send_events_to_market("ACCEPT_OFFER", market_id, offer_parameters.to_dict(), - self._redis_accept_offer_response) + self._send_events_to_market( + "ACCEPT_OFFER", + market_id, + offer_parameters.to_dict(), + self._redis_accept_offer_response, + ) trade = self._trade_buffer self._trade_buffer = None assert trade is not None @@ -160,17 +174,24 @@ def _redis_accept_offer_response(self, payload: dict): else: raise D3ARedisException( f"Error when receiving response on channel {payload['channel']}:: " - f"{data['exception']}: {data['error_message']} {data}") + f"{data['exception']}: {data['error_message']} {data}" + ) def delete_offer(self, market: Union["OneSidedMarket", str], offer: Offer) -> None: """Delete offer from a market""" market_id = market.id if not isinstance(market, str) else market data = {"offer_or_id": offer.to_json_string()} - self._send_events_to_market("DELETE_OFFER", market_id, data, - self._redis_delete_offer_response) + self._send_events_to_market( + "DELETE_OFFER", market_id, data, self._redis_delete_offer_response + ) - def _send_events_to_market(self, event_type_str: str, market_id: Union[str, MarketBase], - data: dict, callback: Callable) -> None: + def _send_events_to_market( + self, + event_type_str: str, + market_id: Union[str, MarketBase], + data: dict, + callback: Callable, + ) -> None: if not isinstance(market_id, str): market_id = market_id.id response_channel = f"{market_id}/{event_type_str}/RESPONSE" @@ -186,8 +207,11 @@ def event_response_was_received_callback(): self.redis.poll_until_response_received(event_response_was_received_callback) if data["transaction_uuid"] not in self._event_response_uuids: - logging.error("Transaction ID not found after %s seconds: %s", - REDIS_PUBLISH_RESPONSE_TIMEOUT, data) + logging.error( + "Transaction ID not found after %s seconds: %s", + REDIS_PUBLISH_RESPONSE_TIMEOUT, + data, + ) else: self._event_response_uuids.remove(data["transaction_uuid"]) @@ -199,7 +223,8 @@ def _redis_delete_offer_response(self, payload: dict) -> None: else: raise D3ARedisException( f"Error when receiving response on channel {payload['channel']}:: " - f"{data['exception']}: {data['error_message']}") + f"{data['exception']}: {data['error_message']}" + ) def _redis_offer_response(self, payload): data = json.loads(payload["data"]) @@ -209,7 +234,8 @@ def _redis_offer_response(self, payload): else: raise D3ARedisException( f"Error when receiving response on channel {payload['channel']}:: " - f"{data['exception']}: {data['error_message']}") + f"{data['exception']}: {data['error_message']}" + ) class MarketStrategyConnectionAdapter: @@ -217,6 +243,7 @@ class MarketStrategyConnectionAdapter: Adapter to the MarketBase class. Used by default when accessing the market object directly and not via Redis. """ + @staticmethod def accept_offer(offer_parameters: AcceptOfferParameters) -> Trade: """Accept an offer on a market.""" @@ -252,7 +279,7 @@ def __init__(self, strategy: "BaseStrategy"): self.split = {} # type: Dict[str, Offer] def _delete_past_offers( - self, existing_offers: Dict[Offer, str], current_time_slot: DateTime + self, existing_offers: Dict[Offer, str], current_time_slot: DateTime ) -> Dict[Offer, str]: offers = {} for offer, market_id in existing_offers.items(): @@ -290,8 +317,9 @@ def sold_offer(self, offer: Offer, market_id: str) -> None: def is_offer_posted(self, market_id: str, offer_id: str) -> bool: """Check if offer is posted on the market""" - return offer_id in [offer.id for offer, _market in self.posted.items() - if market_id == _market] + return offer_id in [ + offer.id for offer, _market in self.posted.items() if market_id == _market + ] def _get_sold_offer_ids_in_market(self, market_id: str) -> List[str]: sold_offer_ids = [] @@ -304,9 +332,11 @@ def open_in_market(self, market_id: str, time_slot: DateTime = None) -> List[Off open_offers = [] sold_offer_ids = self._get_sold_offer_ids_in_market(market_id) for offer, _market_id in self.posted.items(): - if (offer.id not in sold_offer_ids - and market_id == _market_id - and (time_slot is None or offer.time_slot == time_slot)): + if ( + offer.id not in sold_offer_ids + and market_id == _market_id + and (time_slot is None or offer.time_slot == time_slot) + ): open_offers.append(offer) return open_offers @@ -316,27 +346,35 @@ def open_offer_energy(self, market_id: str, time_slot: DateTime = None) -> float def posted_in_market(self, market_id: str, time_slot: DateTime = None) -> List[Offer]: """Get list of posted offers in market""" - return [offer - for offer, _market in self.posted.items() - if market_id == _market and (time_slot is None or offer.time_slot == time_slot)] + return [ + offer + for offer, _market in self.posted.items() + if market_id == _market and (time_slot is None or offer.time_slot == time_slot) + ] def posted_offer_energy(self, market_id: str, time_slot: DateTime = None) -> float: """Get energy of all posted offers""" - return sum(o.energy - for o in self.posted_in_market(market_id, time_slot) - if time_slot is None or o.time_slot == time_slot) + return sum( + o.energy + for o in self.posted_in_market(market_id, time_slot) + if time_slot is None or o.time_slot == time_slot + ) def sold_offer_energy(self, market_id: str, time_slot: DateTime = None) -> float: """Get energy of all sold offers""" - return sum(o.energy - for o in self.sold_in_market(market_id) - if time_slot is None or o.time_slot == time_slot) + return sum( + o.energy + for o in self.sold_in_market(market_id) + if time_slot is None or o.time_slot == time_slot + ) def sold_offer_price(self, market_id: str, time_slot: DateTime = None) -> float: """Get sum of all sold offers' price""" - return sum(o.price - for o in self.sold_in_market(market_id) - if time_slot is None or o.time_slot == time_slot) + return sum( + o.price + for o in self.sold_in_market(market_id) + if time_slot is None or o.time_slot == time_slot + ) def sold_in_market(self, market_id: str) -> List[Offer]: """Get list of sold offers in a market""" @@ -344,9 +382,14 @@ def sold_in_market(self, market_id: str) -> List[Offer]: # pylint: disable=too-many-arguments def can_offer_be_posted( - self, offer_energy: float, offer_price: float, available_energy: float, - market: "MarketBase", replace_existing: bool = False, - time_slot: Optional[DateTime] = None) -> bool: + self, + offer_energy: float, + offer_price: float, + available_energy: float, + market: "MarketBase", + replace_existing: bool = False, + time_slot: Optional[DateTime] = None, + ) -> bool: """ Check whether an offer with the specified parameters can be posted on the market Args: @@ -370,8 +413,9 @@ def can_offer_be_posted( total_posted_energy = offer_energy + posted_offer_energy - return ((total_posted_energy - available_energy) < FLOATING_POINT_TOLERANCE - and offer_price >= 0.0) + return ( + total_posted_energy - available_energy + ) < FLOATING_POINT_TOLERANCE and offer_price >= 0.0 def post(self, offer: Offer, market_id: str) -> None: """Add offer to the posted dict""" @@ -379,8 +423,9 @@ def post(self, offer: Offer, market_id: str) -> None: if offer.id not in self.split: self.posted[offer] = market_id - def remove_offer_from_cache_and_market(self, market: "OneSidedMarket", - offer_id: str = None) -> List[str]: + def remove_offer_from_cache_and_market( + self, market: "OneSidedMarket", offer_id: str = None + ) -> List[str]: """Delete offer from the market and remove it from the dicts""" if offer_id is None: to_delete_offers = self.open_in_market(market.id) @@ -424,8 +469,9 @@ def on_trade(self, market_id: str, trade: Trade) -> None: except AttributeError as ex: raise SimulationException("Trade event before strategy was initialized.") from ex - def on_offer_split(self, original_offer: Offer, accepted_offer: Offer, residual_offer: Offer, - market_id: str) -> None: + def on_offer_split( + self, original_offer: Offer, accepted_offer: Offer, residual_offer: Offer, market_id: str + ) -> None: """React to the event of an offer split""" if original_offer.seller.name == self.strategy.owner.name: self.split[original_offer.id] = accepted_offer @@ -440,6 +486,7 @@ class BaseStrategy(EventMixin, AreaBehaviorBase, ABC): markets, thus removing the need to access the market to view the offers that the strategy has posted. Define a common interface which all strategies should implement. """ + # pylint: disable=too-many-public-methods def __init__(self): super().__init__() @@ -451,8 +498,7 @@ def __init__(self): self._settlement_market_strategy = self._create_settlement_market_strategy() self._future_market_strategy = self._create_future_market_strategy() - @staticmethod - def serialize(): + def serialize(self): """Serialize strategy status.""" return {} @@ -469,8 +515,7 @@ def simulation_config(self) -> SimulationConfig: def _create_settlement_market_strategy(cls): return SettlementMarketStrategyInterface() - @classmethod - def _create_future_market_strategy(cls): + def _create_future_market_strategy(self): return FutureMarketStrategyInterface() def energy_traded(self, market_id: str, time_slot: DateTime = None) -> float: @@ -489,8 +534,10 @@ def trades(self) -> _TradeLookerUpper: @property def _is_eligible_for_balancing_market(self) -> bool: """Check if strategy can participate in the balancing market""" - return (self.owner.name in DeviceRegistry.REGISTRY and - ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET) + return ( + self.owner.name in DeviceRegistry.REGISTRY + and ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET + ) def _remove_existing_offers(self, market: "OneSidedMarket", time_slot: DateTime) -> None: """Remove all existing offers in the market with respect to time_slot.""" @@ -510,10 +557,12 @@ def post_offer(self, market, replace_existing=True, **offer_kwargs) -> Offer: # Remove all existing offers that are still open in the market self._remove_existing_offers(market, offer_kwargs.get("time_slot") or market.time_slot) - if (not offer_kwargs.get("seller") or - not isinstance(offer_kwargs.get("seller"), TraderDetails)): + if not offer_kwargs.get("seller") or not isinstance( + offer_kwargs.get("seller"), TraderDetails + ): offer_kwargs["seller"] = TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid) + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ) if not offer_kwargs.get("time_slot"): offer_kwargs["time_slot"] = market.time_slot @@ -522,13 +571,16 @@ def post_offer(self, market, replace_existing=True, **offer_kwargs) -> Offer: return offer - def post_first_offer(self, market: "OneSidedMarket", energy_kWh: float, - initial_energy_rate: float) -> Optional[Offer]: + def post_first_offer( + self, market: "OneSidedMarket", energy_kWh: float, initial_energy_rate: float + ) -> Optional[Offer]: """Post first and only offer for the strategy. Will fail if another offer already - exists.""" + exists.""" if any(offer.seller.uuid == self.owner.uuid for offer in market.get_offers().values()): - self.owner.log.debug("There is already another offer posted on the market, therefore" - " do not repost another first offer.") + self.owner.log.debug( + "There is already another offer posted on the market, therefore" + " do not repost another first offer." + ) return None return self.post_offer( market, @@ -537,8 +589,9 @@ def post_first_offer(self, market: "OneSidedMarket", energy_kWh: float, energy=energy_kWh, ) - def get_posted_offers(self, market: "OneSidedMarket", - time_slot: Optional[DateTime] = None) -> List[Offer]: + def get_posted_offers( + self, market: "OneSidedMarket", time_slot: Optional[DateTime] = None + ) -> List[Offer]: """Get list of posted offers from a market""" return self.offers.posted_in_market(market.id, time_slot) @@ -560,8 +613,15 @@ def are_offers_posted(self, market_id: str) -> bool: """Checks if any offers have been posted in the market slot with the given ID.""" return len(self.offers.posted_in_market(market_id)) > 0 - def accept_offer(self, market: "OneSidedMarket", offer: Offer, *, buyer: TraderDetails = None, - energy: float = None, trade_bid_info: "TradeBidOfferInfo" = None): + def accept_offer( + self, + market: "OneSidedMarket", + offer: Offer, + *, + buyer: TraderDetails = None, + energy: float = None, + trade_bid_info: "TradeBidOfferInfo" = None, + ): """ Accept an offer on a market. Args: @@ -580,8 +640,7 @@ def accept_offer(self, market: "OneSidedMarket", offer: Offer, *, buyer: TraderD if not isinstance(offer, Offer): offer = market.offers[offer] trade = self._market_adapter.accept_offer( - AcceptOfferParameters( - market, offer, buyer, energy, trade_bid_info) + AcceptOfferParameters(market, offer, buyer, energy, trade_bid_info) ) self.offers.bought_offer(trade.match_details["offer"], market.id) @@ -610,8 +669,14 @@ def event_offer_traded(self, *, market_id: str, trade: Trade) -> None: """ self.offers.on_trade(market_id, trade) - def event_offer_split(self, *, market_id: str, original_offer: Offer, accepted_offer: Offer, - residual_offer: Offer) -> None: + def event_offer_split( + self, + *, + market_id: str, + original_offer: Offer, + accepted_offer: Offer, + residual_offer: Offer, + ) -> None: """React to the event of an offer split""" self.offers.on_offer_split(original_offer, accepted_offer, residual_offer, market_id) @@ -624,23 +689,38 @@ def event_market_cycle(self) -> None: def _assert_if_trade_offer_price_is_too_low(self, market_id: str, trade: Trade) -> None: if trade.is_offer_trade and trade.seller.name == self.owner.name: - offer = [o for o in self.offers.sold[market_id] - if o.id == trade.match_details["offer"].id][0] - assert (trade.trade_rate >= - offer.energy_rate - FLOATING_POINT_TOLERANCE) + offer = [ + o for o in self.offers.sold[market_id] if o.id == trade.match_details["offer"].id + ][0] + assert trade.trade_rate >= offer.energy_rate - FLOATING_POINT_TOLERANCE # pylint: disable=too-many-arguments - def can_offer_be_posted(self, offer_energy: float, offer_price: float, available_energy: float, - market: "OneSidedMarket", time_slot: Optional[DateTime], - replace_existing: bool = False) -> bool: + def can_offer_be_posted( + self, + offer_energy: float, + offer_price: float, + available_energy: float, + market: "OneSidedMarket", + time_slot: Optional[DateTime], + replace_existing: bool = False, + ) -> bool: """Check if an offer with the selected attributes can be posted""" return self.offers.can_offer_be_posted( - offer_energy, offer_price, available_energy, market, time_slot=time_slot, - replace_existing=replace_existing) + offer_energy, + offer_price, + available_energy, + market, + time_slot=time_slot, + replace_existing=replace_existing, + ) def can_settlement_offer_be_posted( - self, offer_energy: float, offer_price: float, - market: "OneSidedMarket", replace_existing: bool = False) -> bool: + self, + offer_energy: float, + offer_price: float, + market: "OneSidedMarket", + replace_existing: bool = False, + ) -> bool: """ Checks whether an offer can be posted to the settlement market :param offer_energy: Energy of the offer that we want to post @@ -654,8 +734,13 @@ def can_settlement_offer_be_posted( return False unsettled_energy_kWh = self.state.get_unsettled_deviation_kWh(market.time_slot) return self.offers.can_offer_be_posted( - offer_energy, offer_price, unsettled_energy_kWh, market, time_slot=market.time_slot, - replace_existing=replace_existing) + offer_energy, + offer_price, + unsettled_energy_kWh, + market, + time_slot=market.time_slot, + replace_existing=replace_existing, + ) @property def spot_market(self) -> "MarketBase": @@ -669,8 +754,9 @@ def spot_market_time_slot(self) -> Optional[DateTime]: return None return self.spot_market.time_slot - def update_offer_rates(self, market: "OneSidedMarket", updated_rate: float, - time_slot: Optional[DateTime] = None) -> None: + def update_offer_rates( + self, market: "OneSidedMarket", updated_rate: float, time_slot: Optional[DateTime] = None + ) -> None: """Update the total price of all offers in the specified market based on their new rate.""" if market.id not in self.offers.open.values(): return @@ -685,12 +771,14 @@ def update_offer_rates(self, market: "OneSidedMarket", updated_rate: float, new_offer = market.offer( updated_price, offer.energy, - TraderDetails(self.owner.name, - self.owner.uuid, - offer.seller.origin, - offer.seller.origin_uuid), + TraderDetails( + self.owner.name, + self.owner.uuid, + offer.seller.origin, + offer.seller.origin_uuid, + ), original_price=updated_price, - time_slot=offer.time_slot or market.time_slot or time_slot + time_slot=offer.time_slot or market.time_slot or time_slot, ) self.offers.replace(offer, new_offer, market.id) except MarketException: @@ -705,6 +793,7 @@ class BidEnabledStrategy(BaseStrategy): Base strategy for all areas / assets that are eligible to post bids and interact with a two sided market """ + def __init__(self): super().__init__() self._bids = {} @@ -733,8 +822,13 @@ def _remove_existing_bids(self, market: MarketBase, time_slot: DateTime) -> None # pylint: disable=too-many-arguments def post_bid( - self, market: MarketBase, price: float, energy: float, replace_existing: bool = True, - time_slot: Optional[DateTime] = None) -> Bid: + self, + market: MarketBase, + price: float, + energy: float, + replace_existing: bool = True, + time_slot: Optional[DateTime] = None, + ) -> Bid: """ Post bid to a specified market. Args: @@ -755,17 +849,16 @@ def post_bid( bid = market.bid( price, energy, - TraderDetails( - self.owner.name, self.owner.uuid, - self.owner.name, self.owner.uuid), + TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid), original_price=price, - time_slot=time_slot or market.time_slot + time_slot=time_slot or market.time_slot, ) self.add_bid_to_posted(market.id, bid) return bid - def update_bid_rates(self, market: "TwoSidedMarket", updated_rate: float, - time_slot: Optional[DateTime] = None) -> None: + def update_bid_rates( + self, market: "TwoSidedMarket", updated_rate: float, time_slot: Optional[DateTime] = None + ) -> None: """Replace the rate of all bids in the market slot with the given updated rate.""" for bid in self.get_posted_bids(market, time_slot): if abs(bid.energy_rate - updated_rate) <= FLOATING_POINT_TOLERANCE: @@ -773,14 +866,24 @@ def update_bid_rates(self, market: "TwoSidedMarket", updated_rate: float, assert bid.buyer.name == self.owner.name self.remove_bid_from_pending(market.id, bid.id) - self.post_bid(market, bid.energy * updated_rate, - bid.energy, replace_existing=False, - time_slot=bid.time_slot) + self.post_bid( + market, + bid.energy * updated_rate, + bid.energy, + replace_existing=False, + time_slot=bid.time_slot, + ) # pylint: disable=too-many-arguments - def can_bid_be_posted(self, bid_energy: float, bid_price: float, required_energy_kWh: float, - market: "TwoSidedMarket", replace_existing: bool = False, - time_slot: Optional[DateTime] = None) -> bool: + def can_bid_be_posted( + self, + bid_energy: float, + bid_price: float, + required_energy_kWh: float, + market: "TwoSidedMarket", + replace_existing: bool = False, + time_slot: Optional[DateTime] = None, + ) -> bool: """Check if a bid can be posted to the market""" if replace_existing: @@ -788,13 +891,17 @@ def can_bid_be_posted(self, bid_energy: float, bid_price: float, required_energy else: posted_bid_energy = self.posted_bid_energy(market.id, time_slot) - total_posted_energy = (bid_energy + posted_bid_energy) + total_posted_energy = bid_energy + posted_bid_energy return total_posted_energy <= required_energy_kWh and bid_price >= 0.0 - def can_settlement_bid_be_posted(self, bid_energy: float, bid_price: float, - market: "TwoSidedMarket", - replace_existing: bool = False) -> bool: + def can_settlement_bid_be_posted( + self, + bid_energy: float, + bid_price: float, + market: "TwoSidedMarket", + replace_existing: bool = False, + ) -> bool: """ Checks whether a bid can be posted to the settlement market :param bid_energy: Energy of the bid that we want to post @@ -808,8 +915,13 @@ def can_settlement_bid_be_posted(self, bid_energy: float, bid_price: float, return False unsettled_energy_kWh = self.state.get_unsettled_deviation_kWh(market.time_slot) return self.can_bid_be_posted( - bid_energy, bid_price, unsettled_energy_kWh, market, time_slot=market.time_slot, - replace_existing=replace_existing) + bid_energy, + bid_price, + unsettled_energy_kWh, + market, + time_slot=market.time_slot, + replace_existing=replace_existing, + ) def is_bid_posted(self, market: "TwoSidedMarket", bid_id: str) -> bool: """Check if bid is posted to the market""" @@ -828,19 +940,25 @@ def posted_bid_energy(self, market_id: str, time_slot: Optional[DateTime] = None """ if market_id not in self._bids: return 0.0 - return sum(b.energy - for b in self._bids[market_id] - if time_slot is None or b.time_slot == time_slot) + return sum( + b.energy + for b in self._bids[market_id] + if time_slot is None or b.time_slot == time_slot + ) def _traded_bid_energy(self, market_id: str, time_slot: Optional[DateTime] = None) -> float: - return sum(b.energy - for b in self._get_traded_bids_from_market(market_id) - if time_slot is None or b.time_slot == time_slot) + return sum( + b.energy + for b in self._get_traded_bids_from_market(market_id) + if time_slot is None or b.time_slot == time_slot + ) def _traded_bid_costs(self, market_id: str, time_slot: Optional[DateTime] = None) -> float: - return sum(b.price - for b in self._get_traded_bids_from_market(market_id) - if time_slot is None or b.time_slot == time_slot) + return sum( + b.price + for b in self._get_traded_bids_from_market(market_id) + if time_slot is None or b.time_slot == time_slot + ) def remove_bid_from_pending(self, market_id: str, bid_id: str = None) -> List[str]: """Remove bid from pending bids dict""" @@ -855,13 +973,14 @@ def remove_bid_from_pending(self, market_id: str, bid_id: str = None) -> List[st for b_id in deleted_bid_ids: if b_id in market.bids.keys(): market.delete_bid(b_id) - self._bids[market.id] = [bid for bid in self.get_posted_bids(market) - if bid.id not in deleted_bid_ids] + self._bids[market.id] = [ + bid for bid in self.get_posted_bids(market) if bid.id not in deleted_bid_ids + ] return deleted_bid_ids def add_bid_to_posted(self, market_id: str, bid: Bid) -> None: """Add bid to posted bids dict""" - if market_id not in self._bids.keys(): + if market_id not in self._bids: self._bids[market_id] = [] self._bids[market_id].append(bid) @@ -885,11 +1004,20 @@ def are_bids_posted(self, market_id: str, time_slot: DateTime = None) -> bool: # time_slot is empty when called for spot markets, where we can retrieve the bids for a # time_slot only by the market_id. For the future markets, the time_slot needs to be # defined for the correct bid selection. - return len([bid for bid in self._bids[market_id] - if time_slot is None or bid.time_slot == time_slot]) > 0 + return ( + len( + [ + bid + for bid in self._bids[market_id] + if time_slot is None or bid.time_slot == time_slot + ] + ) + > 0 + ) - def post_first_bid(self, market: "MarketBase", energy_Wh: float, - initial_energy_rate: float) -> Optional[Bid]: + def post_first_bid( + self, market: "MarketBase", energy_Wh: float, initial_energy_rate: float + ) -> Optional[Bid]: """Post first and only bid for the strategy. Will fail if another bid already exists.""" # It will be safe to remove this check once we remove the event_market_cycle being # called twice, but still it is nice to have it here as a precaution. In general, there @@ -897,8 +1025,10 @@ def post_first_bid(self, market: "MarketBase", energy_Wh: float, # it needs to be updated. If this check is not there, the market cycle event will post # one bid twice, which actually happens on the very first market slot cycle. if any(bid.buyer.name == self.owner.name for bid in market.get_bids().values()): - self.owner.log.debug("There is already another bid posted on the market, therefore" - " do not repost another first bid.") + self.owner.log.debug( + "There is already another bid posted on the market, therefore" + " do not repost another first bid." + ) return None return self.post_bid( market, @@ -907,18 +1037,22 @@ def post_first_bid(self, market: "MarketBase", energy_Wh: float, ) def get_posted_bids( - self, market: "MarketBase", time_slot: Optional[DateTime] = None) -> List[Bid]: + self, market: "MarketBase", time_slot: Optional[DateTime] = None + ) -> List[Bid]: """Get list of posted bids from a market""" if market.id not in self._bids: return [] return [b for b in self._bids[market.id] if time_slot is None or b.time_slot == time_slot] def _assert_bid_can_be_posted_on_market(self, market_id): - assert (ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.TWO_SIDED.value or - self.area.is_market_future(market_id) or - self.area.is_market_settlement(market_id)), ( + assert ( + ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.TWO_SIDED.value + or self.area.is_market_future(market_id) + or self.area.is_market_settlement(market_id) + ), ( "Invalid state, cannot receive a bid if single sided market is globally configured or " - "if it is not a future or settlement market bid.") + "if it is not a future or settlement market bid." + ) def event_bid_deleted(self, *, market_id: str, bid: Bid) -> None: self._assert_bid_can_be_posted_on_market(market_id) @@ -928,8 +1062,9 @@ def event_bid_deleted(self, *, market_id: str, bid: Bid) -> None: self.remove_bid_from_pending(market_id, bid.id) # pylint: disable=unused-argument - def event_bid_split(self, *, market_id: str, original_bid: Bid, accepted_bid: Bid, - residual_bid: Bid) -> None: + def event_bid_split( + self, *, market_id: str, original_bid: Bid, accepted_bid: Bid, residual_bid: Bid + ) -> None: self._assert_bid_can_be_posted_on_market(market_id) if accepted_bid.buyer.name != self.owner.name: @@ -958,8 +1093,7 @@ def _delete_past_bids(self, existing_bids: Dict) -> Dict: updated_bids_dict = {} for market_id, bids in existing_bids.items(): if market_id == self.area.future_markets.id: - updated_bids_dict.update( - {market_id: self._get_future_bids_from_list(bids)}) + updated_bids_dict.update({market_id: self._get_future_bids_from_list(bids)}) return updated_bids_dict def event_market_cycle(self) -> None: @@ -976,8 +1110,11 @@ def assert_if_trade_bid_price_is_too_high(self, market: "MarketBase", trade: "Tr the bid. """ if trade.is_bid_trade and trade.buyer.name == self.owner.name: - bid = [bid for bid in self.get_posted_bids(market) - if bid.id == trade.match_details["bid"].id] + bid = [ + bid + for bid in self.get_posted_bids(market) + if bid.id == trade.match_details["bid"].id + ] if not bid: return assert trade.trade_rate <= bid[0].energy_rate + FLOATING_POINT_TOLERANCE diff --git a/src/gsy_e/models/strategy/external_strategies/load.py b/src/gsy_e/models/strategy/external_strategies/load.py index c61a280123..f57ea4d91f 100644 --- a/src/gsy_e/models/strategy/external_strategies/load.py +++ b/src/gsy_e/models/strategy/external_strategies/load.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import json import logging from typing import TYPE_CHECKING, Callable, Dict, List, Union @@ -25,10 +26,16 @@ from gsy_e.gsy_e_core.exceptions import GSyException from gsy_e.models.strategy.energy_parameters.load import ( - LoadProfileForecastEnergyParams, LoadHoursForecastEnergyParams) -from gsy_e.models.strategy.external_strategies import (CommandTypeNotSupported, ExternalMixin, - ExternalStrategyConnectionManager, - IncomingRequest, OrderCanNotBePosted) + LoadProfileForecastEnergyParams, + LoadHoursForecastEnergyParams, +) +from gsy_e.models.strategy.external_strategies import ( + CommandTypeNotSupported, + ExternalMixin, + ExternalStrategyConnectionManager, + IncomingRequest, + OrderCanNotBePosted, +) from gsy_e.models.strategy.external_strategies.forecast_mixin import ForecastExternalMixin from gsy_e.models.strategy.load_hours import LoadHoursStrategy from gsy_e.models.strategy.predefined_load import DefinedLoadStrategy @@ -58,11 +65,12 @@ class LoadExternalMixin(ExternalMixin): @property def channel_dict(self) -> Dict: """Bid-related Redis API channels.""" - return {**super().channel_dict, - self.channel_names.bid: self.bid, - self.channel_names.delete_bid: self.delete_bid, - self.channel_names.list_bids: self.list_bids, - } + return { + **super().channel_dict, + self.channel_names.bid: self.bid, + self.channel_names.delete_bid: self.delete_bid, + self.channel_names.list_bids: self.list_bids, + } def filtered_market_bids(self, market: "TwoSidedMarket") -> List[Dict]: """ @@ -77,7 +85,8 @@ def filtered_market_bids(self, market: "TwoSidedMarket") -> List[Dict]: return [ {"id": bid.id, "price": bid.price, "energy": bid.energy} for _, bid in market.get_bids().items() - if bid.buyer.name == self.device.name] + if bid.buyer.name == self.device.name + ] def event_activate(self, **kwargs): """Activate the device.""" @@ -89,26 +98,31 @@ def list_bids(self, payload: Dict) -> None: self._get_transaction_id(payload) response_channel = self.channel_names.list_bids_response if not ExternalStrategyConnectionManager.check_for_connected_and_reply( - self.redis, response_channel, self.connected): + self.redis, response_channel, self.connected + ): return arguments = json.loads(payload["data"]) - self.pending_requests.append( - IncomingRequest("list_bids", arguments, response_channel)) + self.pending_requests.append(IncomingRequest("list_bids", arguments, response_channel)) def _list_bids_impl(self, arguments: Dict, response_channel: str) -> None: """Implementation for the list_bids callback, publish this device bids.""" try: market = self._get_market_from_command_argument(arguments) response = { - "command": "list_bids", "status": "ready", - "bid_list": self.filtered_market_bids(market), - "transaction_id": arguments.get("transaction_id")} + "command": "list_bids", + "status": "ready", + "bid_list": self.filtered_market_bids(market), + "transaction_id": arguments.get("transaction_id"), + } except GSyException: error_message = f"Error when handling list bids on area {self.device.name}" logger.exception(error_message) - response = {"command": "list_bids", "status": "error", - "error_message": error_message, - "transaction_id": arguments.get("transaction_id")} + response = { + "command": "list_bids", + "status": "error", + "error_message": error_message, + "transaction_id": arguments.get("transaction_id"), + } self.redis.publish_json(response_channel, response) def delete_bid(self, payload: Dict) -> None: @@ -116,7 +130,8 @@ def delete_bid(self, payload: Dict) -> None: transaction_id = self._get_transaction_id(payload) response_channel = self.channel_names.delete_bid_response if not ExternalStrategyConnectionManager.check_for_connected_and_reply( - self.redis, response_channel, self.connected): + self.redis, response_channel, self.connected + ): return try: arguments = json.loads(payload["data"]) @@ -126,13 +141,17 @@ def delete_bid(self, payload: Dict) -> None: except (GSyException, json.JSONDecodeError) as exception: self.redis.publish_json( response_channel, - {"command": "bid_delete", - "error": f"Incorrect delete bid request. Available parameters: (bid)." - f"Exception: {str(exception)}", - "transaction_id": transaction_id}) + { + "command": "bid_delete", + "error": f"Incorrect delete bid request. Available parameters: (bid)." + f"Exception: {str(exception)}", + "transaction_id": transaction_id, + }, + ) else: self.pending_requests.append( - IncomingRequest("delete_bid", arguments, response_channel)) + IncomingRequest("delete_bid", arguments, response_channel) + ) def _delete_bid_impl(self, arguments: Dict, response_channel: str) -> None: """Implementation for the delete_bid callback, delete the received bid from market.""" @@ -140,47 +159,60 @@ def _delete_bid_impl(self, arguments: Dict, response_channel: str) -> None: market = self._get_market_from_command_argument(arguments) to_delete_bid_id = arguments.get("bid") deleted_bids = self.remove_bid_from_pending(market.id, bid_id=to_delete_bid_id) - response = {"command": "bid_delete", "status": "ready", "deleted_bids": deleted_bids, - "transaction_id": arguments.get("transaction_id")} + response = { + "command": "bid_delete", + "status": "ready", + "deleted_bids": deleted_bids, + "transaction_id": arguments.get("transaction_id"), + } except GSyException: - error_message = (f"Error when handling bid delete on area {self.device.name}: " - f"Bid Arguments: {arguments}, " - "Bid does not exist on the current market.") + error_message = ( + f"Error when handling bid delete on area {self.device.name}: " + f"Bid Arguments: {arguments}, " + "Bid does not exist on the current market." + ) logger.exception(error_message) - response = {"command": "bid_delete", "status": "error", - "error_message": error_message, - "transaction_id": arguments.get("transaction_id")} + response = { + "command": "bid_delete", + "status": "error", + "error_message": error_message, + "transaction_id": arguments.get("transaction_id"), + } self.redis.publish_json(response_channel, response) def bid(self, payload: Dict) -> None: """Callback for bid Redis endpoint.""" transaction_id = self._get_transaction_id(payload) required_args = {"price", "energy", "transaction_id"} - allowed_args = required_args.union({"replace_existing", - "time_slot", - "attributes", - "requirements"}) + allowed_args = required_args.union( + {"replace_existing", "time_slot", "attributes", "requirements"} + ) response_channel = self.channel_names.bid_response if not ExternalStrategyConnectionManager.check_for_connected_and_reply( - self.redis, response_channel, self.connected): + self.redis, response_channel, self.connected + ): return arguments = json.loads(payload["data"]) if ( # Check that all required arguments have been provided - all(arg in arguments.keys() for arg in required_args) - # Check that every provided argument is allowed - and all(arg in allowed_args for arg in arguments.keys())): - self.pending_requests.append( - IncomingRequest("bid", arguments, response_channel)) + all(arg in arguments.keys() for arg in required_args) + # Check that every provided argument is allowed + and all(arg in allowed_args for arg in arguments.keys()) + ): + self.pending_requests.append(IncomingRequest("bid", arguments, response_channel)) else: self.redis.publish_json( response_channel, - {"command": "bid", - "error": ( - "Incorrect bid request. ", - f"Required parameters: {required_args}" - f"Available parameters: {allowed_args}."), - "transaction_id": transaction_id}) + { + "command": "bid", + "error": ( + "Incorrect bid request. ", + f"Required parameters: {required_args}" + f"Available parameters: {allowed_args}.", + ), + "transaction_id": transaction_id, + }, + ) def _bid_impl(self, arguments: Dict, bid_response_channel: str) -> None: """Implementation for the bid callback, post the bid in the market.""" @@ -190,7 +222,8 @@ def _bid_impl(self, arguments: Dict, bid_response_channel: str) -> None: if filtered_fields: response_message = ( "The following arguments are not supported for this market and have been " - f"removed from your order: {filtered_fields}.") + f"removed from your order: {filtered_fields}." + ) market = self._get_market_from_command_argument(arguments) replace_existing = arguments.get("replace_existing", True) @@ -199,27 +232,33 @@ def _bid_impl(self, arguments: Dict, bid_response_channel: str) -> None: arguments["price"], self.get_energy_requirement_kWh_from_market(market), market, - replace_existing=replace_existing) + replace_existing=replace_existing, + ) bid = self.post_bid( - market, - arguments["price"], - arguments["energy"], - replace_existing=replace_existing) + market, arguments["price"], arguments["energy"], replace_existing=replace_existing + ) response = { - "command": "bid", "status": "ready", - "bid": bid.to_json_string(), - "market_type": market.type_name, - "transaction_id": arguments.get("transaction_id"), - "message": response_message} + "command": "bid", + "status": "ready", + "bid": bid.to_json_string(), + "market_type": market.type_name, + "transaction_id": arguments.get("transaction_id"), + "message": response_message, + } except (AssertionError, GSyException): - error_message = (f"Error when handling bid create on area {self.device.name}: " - f"Bid Arguments: {arguments}") + error_message = ( + f"Error when handling bid create on area {self.device.name}: " + f"Bid Arguments: {arguments}" + ) logger.exception(error_message) - response = {"command": "bid", "status": "error", - "error_message": error_message, - "market_type": market.type_name, - "transaction_id": arguments.get("transaction_id")} + response = { + "command": "bid", + "status": "error", + "error_message": error_message, + "market_type": market.type_name, + "transaction_id": arguments.get("transaction_id"), + } self.redis.publish_json(bid_response_channel, response) @property @@ -227,8 +266,10 @@ def _device_info_dict(self) -> Dict: """Return the asset info.""" return { **super()._device_info_dict, - "energy_requirement_kWh": - self.state.get_energy_requirement_Wh(self.spot_market.time_slot) / 1000.0, + "energy_requirement_kWh": self.state.get_energy_requirement_Wh( + self.spot_market.time_slot + ) + / 1000.0, "energy_active_in_bids": self.posted_bid_energy(self.spot_market.id), "energy_traded": self.energy_traded(self.spot_market.id), "total_cost": self.energy_traded_costs(self.spot_market.id), @@ -284,25 +325,30 @@ def _offer_aggregator(self, arguments: Dict) -> Dict: market = self._get_market_from_command_argument(arguments) if self.area.is_market_settlement(market.id): if not self.state.can_post_settlement_offer(market.time_slot): - raise OrderCanNotBePosted("The load did not consume too much energy, ", - "settlement offer can not be posted") - response = ( - self._offer_aggregator_impl( - arguments, market, self._get_time_slot_from_external_arguments(arguments), - self.state.get_unsettled_deviation_kWh( - market.time_slot))) + raise OrderCanNotBePosted( + "The load did not consume too much energy, ", + "settlement offer can not be posted", + ) + response = self._offer_aggregator_impl( + arguments, + market, + self._get_time_slot_from_external_arguments(arguments), + self.state.get_unsettled_deviation_kWh(market.time_slot), + ) else: raise CommandTypeNotSupported("Offer not supported for Loads on spot markets.") except (OrderCanNotBePosted, CommandTypeNotSupported) as ex: response = { - "command": "offer", "status": "error", + "command": "offer", + "status": "error", "market_type": market.type_name, "area_uuid": self.device.uuid, "error_message": "Error when handling offer create " - f"on area {self.device.name} with arguments {arguments}:" - f"{ex}", - "transaction_id": arguments.get("transaction_id")} + f"on area {self.device.name} with arguments {arguments}:" + f"{ex}", + "transaction_id": arguments.get("transaction_id"), + } return response @@ -312,34 +358,48 @@ def _bid_aggregator(self, arguments: Dict) -> Dict: market = self._get_market_from_command_argument(arguments) if self.area.is_market_settlement(market.id): if not self.state.can_post_settlement_bid(market.time_slot): - raise OrderCanNotBePosted("The load did not consume to little energy, " - "settlement bid can not be posted.") + raise OrderCanNotBePosted( + "The load did not consume to little energy, " + "settlement bid can not be posted." + ) required_energy_kWh = self.state.get_unsettled_deviation_kWh(market.time_slot) elif self.area.is_market_future(market.id): - required_energy_kWh = self.state.get_energy_requirement_Wh( - str_to_pendulum_datetime(arguments["time_slot"])) / 1000. + required_energy_kWh = ( + self.state.get_energy_requirement_Wh( + str_to_pendulum_datetime(arguments["time_slot"]) + ) + / 1000.0 + ) elif self.area.is_market_spot(market.id): required_energy_kWh = ( - self.state.get_energy_requirement_Wh(market.time_slot) / 1000.) + self.state.get_energy_requirement_Wh(market.time_slot) / 1000.0 + ) else: - logger.debug("The order cannot be posted on the market. " - "(arguments: %s, market_id: %s", arguments, market.id) + logger.debug( + "The order cannot be posted on the market (arguments: %s, market_id: %s)", + arguments, + market.id, + ) raise OrderCanNotBePosted("The order cannot be posted on the market.") - response = ( - self._bid_aggregator_impl(arguments, market, - self._get_time_slot_from_external_arguments(arguments), - required_energy_kWh)) + response = self._bid_aggregator_impl( + arguments, + market, + self._get_time_slot_from_external_arguments(arguments), + required_energy_kWh, + ) except OrderCanNotBePosted as ex: response = { - "command": "offer", "status": "error", + "command": "offer", + "status": "error", "market_type": market.type_name, "area_uuid": self.device.uuid, "error_message": "Error when handling bid create " - f"on area {self.device.name} with arguments {arguments}:" - f"{ex}", - "transaction_id": arguments.get("transaction_id")} + f"on area {self.device.name} with arguments {arguments}:" + f"{ex}", + "transaction_id": arguments.get("transaction_id"), + } return response @@ -348,21 +408,25 @@ def _delete_bid_aggregator(self, arguments: Dict) -> Dict: try: market = self._get_market_from_command_argument(arguments) to_delete_bid_id = arguments.get("bid") - deleted_bids = ( - self.remove_bid_from_pending(market.id, bid_id=to_delete_bid_id)) + deleted_bids = self.remove_bid_from_pending(market.id, bid_id=to_delete_bid_id) response = { - "command": "bid_delete", "status": "ready", "deleted_bids": deleted_bids, + "command": "bid_delete", + "status": "ready", + "deleted_bids": deleted_bids, "area_uuid": self.device.uuid, - "transaction_id": arguments.get("transaction_id")} + "transaction_id": arguments.get("transaction_id"), + } except GSyException: logger.exception("Error when handling delete bid on area %s", self.device.name) response = { - "command": "bid_delete", "status": "error", + "command": "bid_delete", + "status": "error", "area_uuid": self.device.uuid, "error_message": "Error when handling bid delete " - f"on area {self.device.name} with arguments {arguments}. " - "Bid does not exist on the current market.", - "transaction_id": arguments.get("transaction_id")} + f"on area {self.device.name} with arguments {arguments}. " + "Bid does not exist on the current market.", + "transaction_id": arguments.get("transaction_id"), + } return response def _list_bids_aggregator(self, arguments: Dict) -> Dict: @@ -370,17 +434,21 @@ def _list_bids_aggregator(self, arguments: Dict) -> Dict: try: market = self._get_market_from_command_argument(arguments) response = { - "command": "list_bids", "status": "ready", + "command": "list_bids", + "status": "ready", "bid_list": self.filtered_market_bids(market), "area_uuid": self.device.uuid, - "transaction_id": arguments.get("transaction_id")} + "transaction_id": arguments.get("transaction_id"), + } except GSyException: logger.exception("Error when handling list bids on area %s", self.device.name) response = { - "command": "list_bids", "status": "error", + "command": "list_bids", + "status": "error", "area_uuid": self.device.uuid, "error_message": f"Error when listing bids on area {self.device.name}.", - "transaction_id": arguments.get("transaction_id")} + "transaction_id": arguments.get("transaction_id"), + } return response @@ -394,7 +462,7 @@ class LoadProfileExternalStrategy(LoadExternalMixin, DefinedLoadStrategy): class LoadForecastExternalStrategyMixin(ForecastExternalMixin): """ - Strategy responsible for reading forecast and measurement consumption data via hardware API + Strategy responsible for reading forecast and measurement consumption data via hardware API """ def update_energy_forecast(self) -> None: @@ -422,82 +490,113 @@ def _set_energy_measurement_of_last_market(self): class LoadProfileForecastExternalStrategy( - LoadForecastExternalStrategyMixin, LoadProfileExternalStrategy): + LoadForecastExternalStrategyMixin, LoadProfileExternalStrategy +): """ - Strategy responsible for reading forecast and measurement consumption data via hardware - API. In case the hardware API is not available the normal profile strategy will be used - instead. + Strategy responsible for reading forecast and measurement consumption data via hardware + API. In case the hardware API is not available the normal profile strategy will be used + instead. """ + # pylint: disable=too-many-arguments - def __init__(self, fit_to_limit=True, energy_rate_increase_per_update=None, - update_interval=None, - initial_buying_rate: Union[float, dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, - final_buying_rate: Union[float, dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, - balancing_energy_ratio: tuple = - (ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, - ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), - use_market_maker_rate: bool = False, - daily_load_profile=None, - daily_load_profile_uuid=None): + def __init__( + self, + fit_to_limit=True, + energy_rate_increase_per_update=None, + update_interval=None, + initial_buying_rate: Union[ + float, dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, + final_buying_rate: Union[ + float, dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, + balancing_energy_ratio: tuple = ( + ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, + ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO, + ), + use_market_maker_rate: bool = False, + daily_load_profile=None, + daily_load_profile_uuid=None, + **kwargs, + ): """ Constructor of LoadForecastStrategy """ + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") + if update_interval is None: update_interval = duration( - minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) - - super().__init__(daily_load_profile=None, - fit_to_limit=fit_to_limit, - energy_rate_increase_per_update=energy_rate_increase_per_update, - update_interval=update_interval, - final_buying_rate=final_buying_rate, - initial_buying_rate=initial_buying_rate, - balancing_energy_ratio=balancing_energy_ratio, - use_market_maker_rate=use_market_maker_rate, - daily_load_profile_uuid=daily_load_profile_uuid, - ) + minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL + ) + + super().__init__( + daily_load_profile=None, + fit_to_limit=fit_to_limit, + energy_rate_increase_per_update=energy_rate_increase_per_update, + update_interval=update_interval, + final_buying_rate=final_buying_rate, + initial_buying_rate=initial_buying_rate, + balancing_energy_ratio=balancing_energy_ratio, + use_market_maker_rate=use_market_maker_rate, + daily_load_profile_uuid=daily_load_profile_uuid, + ) self._energy_params = LoadProfileForecastEnergyParams( - daily_load_profile, daily_load_profile_uuid) + daily_load_profile, daily_load_profile_uuid + ) class LoadHoursForecastExternalStrategy( - LoadForecastExternalStrategyMixin, LoadHoursExternalStrategy): + LoadForecastExternalStrategyMixin, LoadHoursExternalStrategy +): """ - Strategy responsible for reading forecast and measurement consumption data via hardware - API. In case the hardware API is not available the normal load hours strategy will be used - instead. + Strategy responsible for reading forecast and measurement consumption data via hardware + API. In case the hardware API is not available the normal load hours strategy will be used + instead. """ + # pylint: disable=too-many-arguments,unused-argument - def __init__(self, fit_to_limit=True, energy_rate_increase_per_update=None, - update_interval=None, - initial_buying_rate: Union[float, dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, - final_buying_rate: Union[float, dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, - balancing_energy_ratio: tuple = - (ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, - ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), - use_market_maker_rate: bool = False, - avg_power_W=0, - hrs_of_day=None): + def __init__( + self, + fit_to_limit=True, + energy_rate_increase_per_update=None, + update_interval=None, + initial_buying_rate: Union[ + float, dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, + final_buying_rate: Union[ + float, dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, + balancing_energy_ratio: tuple = ( + ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, + ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO, + ), + use_market_maker_rate: bool = False, + avg_power_W=0, + hrs_of_day=None, + **kwargs, + ): """ Constructor of LoadForecastStrategy """ + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") + if update_interval is None: update_interval = duration( - minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) - - super().__init__(None, - fit_to_limit=fit_to_limit, - energy_rate_increase_per_update=energy_rate_increase_per_update, - update_interval=update_interval, - final_buying_rate=final_buying_rate, - initial_buying_rate=initial_buying_rate, - balancing_energy_ratio=balancing_energy_ratio, - use_market_maker_rate=use_market_maker_rate) - - self._energy_params = LoadHoursForecastEnergyParams( - avg_power_W, hrs_of_day) + minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL + ) + + super().__init__( + None, + fit_to_limit=fit_to_limit, + energy_rate_increase_per_update=energy_rate_increase_per_update, + update_interval=update_interval, + final_buying_rate=final_buying_rate, + initial_buying_rate=initial_buying_rate, + balancing_energy_ratio=balancing_energy_ratio, + use_market_maker_rate=use_market_maker_rate, + ) + + self._energy_params = LoadHoursForecastEnergyParams(avg_power_W, hrs_of_day) diff --git a/src/gsy_e/models/strategy/external_strategies/pv.py b/src/gsy_e/models/strategy/external_strategies/pv.py index a8b84e90b0..aa521062b6 100644 --- a/src/gsy_e/models/strategy/external_strategies/pv.py +++ b/src/gsy_e/models/strategy/external_strategies/pv.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import json import logging from typing import TYPE_CHECKING, Callable, Dict @@ -25,9 +26,13 @@ from pendulum import duration from gsy_e.gsy_e_core.exceptions import GSyException -from gsy_e.models.strategy.external_strategies import (CommandTypeNotSupported, ExternalMixin, - ExternalStrategyConnectionManager, - IncomingRequest, OrderCanNotBePosted) +from gsy_e.models.strategy.external_strategies import ( + CommandTypeNotSupported, + ExternalMixin, + ExternalStrategyConnectionManager, + IncomingRequest, + OrderCanNotBePosted, +) from gsy_e.models.strategy.external_strategies.forecast_mixin import ForecastExternalMixin from gsy_e.models.strategy.predefined_pv import PVPredefinedStrategy, PVUserProfileStrategy from gsy_e.models.strategy.pv import PVStrategy @@ -45,6 +50,7 @@ class PVExternalMixin(ExternalMixin): Mixin for enabling an external api for the PV strategies. Should always be inherited together with a superclass of PVStrategy. """ + state: "PVState" offers: "Offers" can_offer_be_posted: Callable @@ -55,11 +61,12 @@ class PVExternalMixin(ExternalMixin): @property def channel_dict(self) -> Dict: """Offer-related Redis API channels.""" - return {**super().channel_dict, - self.channel_names.offer: self.offer, - self.channel_names.delete_offer: self.delete_offer, - self.channel_names.list_offers: self.list_offers, - } + return { + **super().channel_dict, + self.channel_names.offer: self.offer, + self.channel_names.delete_offer: self.delete_offer, + self.channel_names.list_offers: self.list_offers, + } def event_activate(self, **kwargs) -> None: """Activate the device.""" @@ -71,28 +78,36 @@ def list_offers(self, payload: Dict) -> None: self._get_transaction_id(payload) response_channel = self.channel_names.list_offers_response if not ExternalStrategyConnectionManager.check_for_connected_and_reply( - self.redis, response_channel, self.connected): + self.redis, response_channel, self.connected + ): return arguments = json.loads(payload["data"]) - self.pending_requests.append( - IncomingRequest("list_offers", arguments, response_channel)) + self.pending_requests.append(IncomingRequest("list_offers", arguments, response_channel)) def _list_offers_impl(self, arguments: Dict, response_channel: str) -> None: """Implementation for the list_offers callback, publish this device offers.""" try: market = self._get_market_from_command_argument(arguments) - filtered_offers = [{"id": v.id, "price": v.price, "energy": v.energy} - for _, v in market.get_offers().items() - if v.seller.name == self.device.name] - response = {"command": "list_offers", "status": "ready", - "offer_list": filtered_offers, - "transaction_id": arguments.get("transaction_id")} + filtered_offers = [ + {"id": v.id, "price": v.price, "energy": v.energy} + for _, v in market.get_offers().items() + if v.seller.name == self.device.name + ] + response = { + "command": "list_offers", + "status": "ready", + "offer_list": filtered_offers, + "transaction_id": arguments.get("transaction_id"), + } except GSyException: error_message = f"Error when handling list offers on area {self.device.name}" logger.exception(error_message) - response = {"command": "list_offers", "status": "error", - "error_message": error_message, - "transaction_id": arguments.get("transaction_id")} + response = { + "command": "list_offers", + "status": "error", + "error_message": error_message, + "transaction_id": arguments.get("transaction_id"), + } self.redis.publish_json(response_channel, response) def delete_offer(self, payload: Dict) -> None: @@ -100,24 +115,30 @@ def delete_offer(self, payload: Dict) -> None: transaction_id = self._get_transaction_id(payload) response_channel = self.channel_names.delete_offer_response if not ExternalStrategyConnectionManager.check_for_connected_and_reply( - self.redis, response_channel, self.connected): + self.redis, response_channel, self.connected + ): return try: arguments = json.loads(payload["data"]) market = self._get_market_from_command_argument(arguments) if arguments.get("offer") and not self.offers.is_offer_posted( - market.id, arguments["offer"]): + market.id, arguments["offer"] + ): raise GSyException("Offer_id is not associated with any posted offer.") except (GSyException, json.JSONDecodeError): logger.exception("Error when handling delete offer request. Payload %s", payload) self.redis.publish_json( response_channel, - {"command": "offer_delete", - "error": "Incorrect delete offer request. Available parameters: (offer).", - "transaction_id": transaction_id}) + { + "command": "offer_delete", + "error": "Incorrect delete offer request. Available parameters: (offer).", + "transaction_id": transaction_id, + }, + ) else: self.pending_requests.append( - IncomingRequest("delete_offer", arguments, response_channel)) + IncomingRequest("delete_offer", arguments, response_channel) + ) def _delete_offer_impl(self, arguments: Dict, response_channel: str) -> None: """Implementation for the delete_offer callback, delete the received offer from market.""" @@ -125,29 +146,38 @@ def _delete_offer_impl(self, arguments: Dict, response_channel: str) -> None: market = self._get_market_from_command_argument(arguments) to_delete_offer_id = arguments.get("offer") deleted_offers = self.offers.remove_offer_from_cache_and_market( - market, to_delete_offer_id) - response = {"command": "offer_delete", "status": "ready", - "deleted_offers": deleted_offers, - "transaction_id": arguments.get("transaction_id")} + market, to_delete_offer_id + ) + response = { + "command": "offer_delete", + "status": "ready", + "deleted_offers": deleted_offers, + "transaction_id": arguments.get("transaction_id"), + } except GSyException: - error_message = (f"Error when handling offer delete on area {self.device.name}: " - f"Offer Arguments: {arguments}") + error_message = ( + f"Error when handling offer delete on area {self.device.name}: " + f"Offer Arguments: {arguments}" + ) logger.exception(error_message) - response = {"command": "offer_delete", "status": "error", - "error_message": error_message, - "transaction_id": arguments.get("transaction_id")} + response = { + "command": "offer_delete", + "status": "error", + "error_message": error_message, + "transaction_id": arguments.get("transaction_id"), + } self.redis.publish_json(response_channel, response) def offer(self, payload: Dict) -> None: """Callback for offer Redis endpoint.""" transaction_id = self._get_transaction_id(payload) required_args = {"price", "energy", "transaction_id"} - allowed_args = required_args.union({"replace_existing", - "time_slot"}) + allowed_args = required_args.union({"replace_existing", "time_slot"}) response_channel = self.channel_names.offer_response if not ExternalStrategyConnectionManager.check_for_connected_and_reply( - self.redis, response_channel, self.connected): + self.redis, response_channel, self.connected + ): return try: arguments = json.loads(payload["data"]) @@ -161,15 +191,18 @@ def offer(self, payload: Dict) -> None: logger.exception("Incorrect offer request. Payload %s.", payload) self.redis.publish_json( response_channel, - {"command": "offer", - "error": ( - "Incorrect bid request. ", - f"Required parameters: {required_args}" - f"Available parameters: {allowed_args}."), - "transaction_id": transaction_id}) + { + "command": "offer", + "error": ( + "Incorrect bid request. ", + f"Required parameters: {required_args}" + f"Available parameters: {allowed_args}.", + ), + "transaction_id": transaction_id, + }, + ) else: - self.pending_requests.append( - IncomingRequest("offer", arguments, response_channel)) + self.pending_requests.append(IncomingRequest("offer", arguments, response_channel)) def _offer_impl(self, arguments: Dict, response_channel: str) -> None: try: @@ -180,36 +213,48 @@ def _offer_impl(self, arguments: Dict, response_channel: str) -> None: arguments["price"], self.state.get_available_energy_kWh(market.time_slot), market, - replace_existing=replace_existing) + replace_existing=replace_existing, + ) offer_arguments = { - k: v for k, v in arguments.items() if k not in ["transaction_id", "time_slot"]} - offer = self.post_offer( - market, replace_existing=replace_existing, **offer_arguments) + k: v for k, v in arguments.items() if k not in ["transaction_id", "time_slot"] + } + offer = self.post_offer(market, replace_existing=replace_existing, **offer_arguments) self.redis.publish_json( response_channel, - {"command": "offer", "status": "ready", - "market_type": market.type_name, - "offer": offer.to_json_string(), - "transaction_id": arguments.get("transaction_id")}) + { + "command": "offer", + "status": "ready", + "market_type": market.type_name, + "offer": offer.to_json_string(), + "transaction_id": arguments.get("transaction_id"), + }, + ) except (AssertionError, GSyException): - error_message = (f"Error when handling offer create on area {self.device.name}: " - f"Offer Arguments: {arguments}") + error_message = ( + f"Error when handling offer create on area {self.device.name}: " + f"Offer Arguments: {arguments}" + ) logger.exception(error_message) self.redis.publish_json( response_channel, - {"command": "offer", "status": "error", - "market_type": market.type_name, - "error_message": error_message, - "transaction_id": arguments.get("transaction_id")}) + { + "command": "offer", + "status": "error", + "market_type": market.type_name, + "error_message": error_message, + "transaction_id": arguments.get("transaction_id"), + }, + ) @property def _device_info_dict(self): return { **super()._device_info_dict, - "available_energy_kWh": - self.state.get_available_energy_kWh(self.spot_market.time_slot), + "available_energy_kWh": self.state.get_available_energy_kWh( + self.spot_market.time_slot + ), "energy_active_in_offers": self.offers.open_offer_energy(self.spot_market.id), "energy_traded": self.energy_traded(self.spot_market.id), "total_cost": self.energy_traded_costs(self.spot_market.id), @@ -266,44 +311,56 @@ def _delete_offer_aggregator(self, arguments: Dict) -> Dict: """Callback for the delete offer endpoint when sent by aggregator.""" market = self._get_market_from_command_argument(arguments) if arguments.get("offer") and not self.offers.is_offer_posted( - market.id, arguments["offer"]): + market.id, arguments["offer"] + ): raise GSyException("Offer_id is not associated with any posted offer.") try: to_delete_offer_id = arguments.get("offer") deleted_offers = self.offers.remove_offer_from_cache_and_market( - market, to_delete_offer_id) + market, to_delete_offer_id + ) response = { - "command": "offer_delete", "status": "ready", + "command": "offer_delete", + "status": "ready", "area_uuid": self.device.uuid, "deleted_offers": deleted_offers, - "transaction_id": arguments.get("transaction_id") + "transaction_id": arguments.get("transaction_id"), } except GSyException: response = { - "command": "offer_delete", "status": "error", + "command": "offer_delete", + "status": "error", "area_uuid": self.device.uuid, "error_message": f"Error when handling offer delete " - f"on area {self.device.name} with arguments {arguments}.", - "transaction_id": arguments.get("transaction_id")} + f"on area {self.device.name} with arguments {arguments}.", + "transaction_id": arguments.get("transaction_id"), + } return response def _list_offers_aggregator(self, arguments: Dict) -> Dict: try: market = self._get_market_from_command_argument(arguments) - filtered_offers = [{"id": v.id, "price": v.price, "energy": v.energy} - for v in market.get_offers().values() - if v.seller.name == self.device.name] + filtered_offers = [ + {"id": v.id, "price": v.price, "energy": v.energy} + for v in market.get_offers().values() + if v.seller.name == self.device.name + ] response = { - "command": "list_offers", "status": "ready", "offer_list": filtered_offers, + "command": "list_offers", + "status": "ready", + "offer_list": filtered_offers, "area_uuid": self.device.uuid, - "transaction_id": arguments.get("transaction_id")} + "transaction_id": arguments.get("transaction_id"), + } except GSyException: response = { - "command": "list_offers", "status": "error", + "command": "list_offers", + "status": "error", "area_uuid": self.device.uuid, "error_message": f"Error when listing offers on area {self.device.name}.", - "transaction_id": arguments.get("transaction_id")} + "transaction_id": arguments.get("transaction_id"), + } return response def _bid_aggregator(self, arguments: Dict) -> Dict: @@ -312,26 +369,30 @@ def _bid_aggregator(self, arguments: Dict) -> Dict: market = self._get_market_from_command_argument(arguments) if self.area.is_market_settlement(market.id): if not self.state.can_post_settlement_bid(market.time_slot): - raise OrderCanNotBePosted("The PV did not produce too little energy, " - "settlement bid can not be posted.") - response = ( - self._bid_aggregator_impl(arguments, market, - self._get_time_slot_from_external_arguments( - arguments), - self.state.get_unsettled_deviation_kWh( - market.time_slot))) + raise OrderCanNotBePosted( + "The PV did not produce too little energy, " + "settlement bid can not be posted." + ) + response = self._bid_aggregator_impl( + arguments, + market, + self._get_time_slot_from_external_arguments(arguments), + self.state.get_unsettled_deviation_kWh(market.time_slot), + ) else: raise CommandTypeNotSupported("Bid not supported for PV on spot markets.") except (OrderCanNotBePosted, CommandTypeNotSupported) as ex: response = { - "command": "offer", "status": "error", + "command": "offer", + "status": "error", "area_uuid": self.device.uuid, "market_type": market.type_name, "error_message": "Error when handling offer create " - f"on area {self.device.name} with arguments {arguments}:" - f"{ex}", - "transaction_id": arguments.get("transaction_id")} + f"on area {self.device.name} with arguments {arguments}:" + f"{ex}", + "transaction_id": arguments.get("transaction_id"), + } return response @@ -341,33 +402,43 @@ def _offer_aggregator(self, arguments: Dict) -> Dict: market = self._get_market_from_command_argument(arguments) if self.area.is_market_settlement(market.id): if not self.state.can_post_settlement_offer(market.time_slot): - raise OrderCanNotBePosted("The PV did not produce too much energy, " - "settlement offer can not be posted.") + raise OrderCanNotBePosted( + "The PV did not produce too much energy, " + "settlement offer can not be posted." + ) available_energy_kWh = self.state.get_unsettled_deviation_kWh(market.time_slot) elif self.area.is_market_future(market.id): available_energy_kWh = self.state.get_available_energy_kWh( - str_to_pendulum_datetime(arguments["time_slot"])) + str_to_pendulum_datetime(arguments["time_slot"]) + ) elif self.area.is_market_spot(market.id): available_energy_kWh = self.state.get_available_energy_kWh(market.time_slot) else: - logger.debug("The order cannot be posted on the market. " - "(arguments: %s, market_id: %s", arguments, market.id) + logger.debug( + "The order cannot be posted on the market (arguments: %s, market_id: %s)", + arguments, + market.id, + ) raise OrderCanNotBePosted("The order cannot be posted on the market.") - response = ( - self._offer_aggregator_impl(arguments, market, - self._get_time_slot_from_external_arguments(arguments), - available_energy_kWh)) + response = self._offer_aggregator_impl( + arguments, + market, + self._get_time_slot_from_external_arguments(arguments), + available_energy_kWh, + ) except OrderCanNotBePosted as ex: response = { - "command": "offer", "status": "error", + "command": "offer", + "status": "error", "market_type": market.type_name, "area_uuid": self.device.uuid, "error_message": "Error when handling offer create " - f"on area {self.device.name} with arguments {arguments}:" - f"{ex}", - "transaction_id": arguments.get("transaction_id")} + f"on area {self.device.name} with arguments {arguments}:" + f"{ex}", + "transaction_id": arguments.get("transaction_id"), + } return response @@ -391,27 +462,33 @@ class PVForecastExternalStrategy(ForecastExternalMixin, PVPredefinedExternalStra # pylint: disable=too-many-arguments,unused-argument def __init__( - self, panel_count=1, - initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - final_selling_rate: float = ConstSettings.PVSettings.SELLING_RATE_RANGE.final, - fit_to_limit: bool = True, - update_interval=duration( - minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), - energy_rate_decrease_per_update=None, - use_market_maker_rate: bool = False, - cloud_coverage: int = None, - capacity_kW: float = None + self, + panel_count=1, + initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + final_selling_rate: float = ConstSettings.PVSettings.SELLING_RATE_RANGE.final, + fit_to_limit: bool = True, + update_interval=duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), + energy_rate_decrease_per_update=None, + use_market_maker_rate: bool = False, + cloud_coverage: int = None, + capacity_kW: float = None, + **kwargs, ): """ Constructor of PVForecastStrategy """ - super().__init__(panel_count=panel_count, - initial_selling_rate=initial_selling_rate, - final_selling_rate=final_selling_rate, - fit_to_limit=fit_to_limit, - update_interval=update_interval, - energy_rate_decrease_per_update=energy_rate_decrease_per_update, - use_market_maker_rate=use_market_maker_rate) + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") + + super().__init__( + panel_count=panel_count, + initial_selling_rate=initial_selling_rate, + final_selling_rate=final_selling_rate, + fit_to_limit=fit_to_limit, + update_interval=update_interval, + energy_rate_decrease_per_update=energy_rate_decrease_per_update, + use_market_maker_rate=use_market_maker_rate, + ) def update_energy_forecast(self) -> None: """Set energy forecast for future markets.""" @@ -432,6 +509,6 @@ def set_produced_energy_forecast_in_state(self, reconfigure=False) -> None: def _set_energy_measurement_of_last_market(self): """ - Setting energy measurement for the previous slot is already done by - update_energy_measurement - """ + Setting energy measurement for the previous slot is already done by + update_energy_measurement + """ diff --git a/src/gsy_e/models/strategy/load_hours.py b/src/gsy_e/models/strategy/load_hours.py index 2073aec5eb..24c1fde44d 100644 --- a/src/gsy_e/models/strategy/load_hours.py +++ b/src/gsy_e/models/strategy/load_hours.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 collections import namedtuple from typing import Union, Dict @@ -24,8 +25,10 @@ from gsy_framework.exceptions import GSyDeviceException from gsy_framework.read_user_profile import read_arbitrary_profile, InputProfileTypes from gsy_framework.utils import ( - limit_float_precision, get_from_profile_same_weekday_and_time, - is_time_slot_in_simulation_duration) + limit_float_precision, + get_from_profile_same_weekday_and_time, + is_time_slot_in_simulation_duration, +) from gsy_framework.validators.load_validator import LoadValidator from numpy import random from pendulum import duration @@ -56,21 +59,30 @@ def serialize(self): **self._energy_params.serialize(), **self.bid_update.serialize(), "balancing_energy_ratio": self.balancing_energy_ratio, - "use_market_maker_rate": self.use_market_maker_rate + "use_market_maker_rate": self.use_market_maker_rate, } # pylint: disable=too-many-arguments - def __init__(self, avg_power_W, hrs_of_day=None, - fit_to_limit=True, energy_rate_increase_per_update=None, - update_interval=None, - initial_buying_rate: Union[float, Dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, - final_buying_rate: Union[float, Dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, - balancing_energy_ratio: tuple = - (ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, - ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), - use_market_maker_rate: bool = False): + def __init__( + self, + avg_power_W, + hrs_of_day=None, + fit_to_limit=True, + energy_rate_increase_per_update=None, + update_interval=None, + initial_buying_rate: Union[ + float, Dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, + final_buying_rate: Union[ + float, Dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, + balancing_energy_ratio: tuple = ( + ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, + ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO, + ), + use_market_maker_rate: bool = False, + **kwargs + ): """ Constructor of LoadHoursStrategy :param avg_power_W: Power rating of load device @@ -84,13 +96,20 @@ def __init__(self, avg_power_W, hrs_of_day=None, :param use_market_maker_rate: If set to True, Load would track its final buying rate as per utility's trading rate """ + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") super().__init__() self._energy_params = LoadHoursEnergyParameters(avg_power_W, hrs_of_day) self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio) self.use_market_maker_rate = use_market_maker_rate - self._init_price_update(fit_to_limit, energy_rate_increase_per_update, update_interval, - initial_buying_rate, final_buying_rate) + self._init_price_update( + fit_to_limit, + energy_rate_increase_per_update, + update_interval, + initial_buying_rate, + final_buying_rate, + ) self._calculate_active_markets() self._cycled_market = set() @@ -107,46 +126,65 @@ def _create_settlement_market_strategy(cls): def _create_future_market_strategy(self): return future_market_strategy_factory(self.asset_type) - def _init_price_update(self, fit_to_limit, energy_rate_increase_per_update, update_interval, - initial_buying_rate, final_buying_rate): + def _init_price_update( + self, + fit_to_limit, + energy_rate_increase_per_update, + update_interval, + initial_buying_rate, + final_buying_rate, + ): LoadValidator.validate_rate( fit_to_limit=fit_to_limit, - energy_rate_increase_per_update=energy_rate_increase_per_update) + energy_rate_increase_per_update=energy_rate_increase_per_update, + ) if update_interval is None: update_interval = duration( - minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) + minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL + ) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.bid_update = TemplateStrategyBidUpdater( - initial_rate=initial_buying_rate, final_rate=final_buying_rate, + initial_rate=initial_buying_rate, + final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, - update_interval=update_interval, rate_limit_object=min) + update_interval=update_interval, + rate_limit_object=min, + ) - def _validate_rates(self, initial_rate, final_rate, energy_rate_change_per_update, - fit_to_limit): + def _validate_rates( + self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ): # all parameters have to be validated for each time slot starting from the current time for time_slot in initial_rate.keys(): if not is_time_slot_in_simulation_duration(time_slot, self.area.config): continue - if (self.area and - self.area.current_market - and time_slot < self.area.current_market.time_slot): + if ( + self.area + and self.area.current_market + and time_slot < self.area.current_market.time_slot + ): continue - rate_change = (None if fit_to_limit else - get_from_profile_same_weekday_and_time( - energy_rate_change_per_update, time_slot)) + rate_change = ( + None + if fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_change_per_update, time_slot + ) + ) LoadValidator.validate_rate( initial_buying_rate=initial_rate[time_slot], energy_rate_increase_per_update=rate_change, final_buying_rate=get_from_profile_same_weekday_and_time(final_rate, time_slot), - fit_to_limit=fit_to_limit) + fit_to_limit=fit_to_limit, + ) def event_activate(self, **kwargs): self._energy_params.event_activate_energy(self.area) @@ -185,32 +223,37 @@ def _set_energy_measurement_of_last_market(self): self._energy_params.set_energy_measurement_kWh(self.area.current_market.time_slot) def _delete_past_state(self): - if (constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True or - self.area.current_market is None): + if ( + constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True + or self.area.current_market is None + ): return self.state.delete_past_state_values(self.area.current_market.time_slot) self.bid_update.delete_past_state_values(self.area.current_market.time_slot) - self._future_market_strategy.delete_past_state_values( - self.area.current_market.time_slot) + self._future_market_strategy.delete_past_state_values(self.area.current_market.time_slot) def _area_reconfigure_prices(self, **kwargs): if kwargs.get("initial_buying_rate") is not None: - initial_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["initial_buying_rate"]) + initial_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["initial_buying_rate"] + ) else: initial_rate = self.bid_update.initial_rate_profile_buffer if kwargs.get("final_buying_rate") is not None: - final_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["final_buying_rate"]) + final_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["final_buying_rate"] + ) else: final_rate = self.bid_update.final_rate_profile_buffer if kwargs.get("energy_rate_increase_per_update") is not None: energy_rate_change_per_update = read_arbitrary_profile( - InputProfileTypes.IDENTITY, kwargs["energy_rate_increase_per_update"]) + InputProfileTypes.IDENTITY, kwargs["energy_rate_increase_per_update"] + ) else: - energy_rate_change_per_update = (self.bid_update. - energy_rate_change_per_update_profile_buffer) + energy_rate_change_per_update = ( + self.bid_update.energy_rate_change_per_update_profile_buffer + ) if kwargs.get("fit_to_limit") is not None: fit_to_limit = kwargs["fit_to_limit"] else: @@ -219,7 +262,8 @@ def _area_reconfigure_prices(self, **kwargs): update_interval = ( duration(minutes=kwargs["update_interval"]) if isinstance(kwargs["update_interval"], int) - else kwargs["update_interval"]) + else kwargs["update_interval"] + ) else: update_interval = self.bid_update.update_interval @@ -227,8 +271,9 @@ def _area_reconfigure_prices(self, **kwargs): self.use_market_maker_rate = kwargs["use_market_maker_rate"] try: - self._validate_rates(initial_rate, final_rate, energy_rate_change_per_update, - fit_to_limit) + self._validate_rates( + initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ) except GSyDeviceException: self.log.exception("LoadHours._area_reconfigure_prices failed. Exception: ") return @@ -238,7 +283,7 @@ def _area_reconfigure_prices(self, **kwargs): final_rate=final_rate, energy_rate_change_per_update=energy_rate_change_per_update, fit_to_limit=fit_to_limit, - update_interval=update_interval + update_interval=update_interval, ) def area_reconfigure_event(self, *args, **kwargs): @@ -252,10 +297,12 @@ def event_activate_price(self): """Update the strategy prices upon the activation and validate them afterwards.""" self._replace_rates_with_market_maker_rates() - self._validate_rates(self.bid_update.initial_rate_profile_buffer, - self.bid_update.final_rate_profile_buffer, - self.bid_update.energy_rate_change_per_update_profile_buffer, - self.bid_update.fit_to_limit) + self._validate_rates( + self.bid_update.initial_rate_profile_buffer, + self.bid_update.final_rate_profile_buffer, + self.bid_update.energy_rate_change_per_update_profile_buffer, + self.bid_update.fit_to_limit, + ) @staticmethod def _find_acceptable_offer(market): @@ -267,7 +314,8 @@ def _offer_rate_can_be_accepted(self, offer: Offer, market_slot: MarketBase): max_affordable_offer_rate = self.bid_update.get_updated_rate(market_slot.time_slot) return ( limit_float_precision(offer.energy_rate) - <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE) + <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE + ) def _one_sided_market_event_tick(self, market, offer=None): if not self.state.can_buy_more_energy(market.time_slot): @@ -284,20 +332,26 @@ def _one_sided_market_event_tick(self, market, offer=None): acceptable_offer = offer time_slot = market.time_slot - if (acceptable_offer and self._energy_params.allowed_operating_hours(time_slot) - and self._offer_rate_can_be_accepted(acceptable_offer, market)): + if ( + acceptable_offer + and self._energy_params.allowed_operating_hours(time_slot) + and self._offer_rate_can_be_accepted(acceptable_offer, market) + ): energy_Wh = self.state.calculate_energy_to_accept( - acceptable_offer.energy * 1000.0, time_slot) - self.accept_offer(market, acceptable_offer, - buyer=TraderDetails( - self.owner.name, self.owner.uuid, - self.owner.name, self.owner.uuid), - energy=energy_Wh / 1000.0) + acceptable_offer.energy * 1000.0, time_slot + ) + self.accept_offer( + market, + acceptable_offer, + buyer=TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), + energy=energy_Wh / 1000.0, + ) self._energy_params.decrement_energy_requirement( - energy_kWh=energy_Wh / 1000, - time_slot=time_slot, - area_name=self.owner.name) + energy_kWh=energy_Wh / 1000, time_slot=time_slot, area_name=self.owner.name + ) except MarketException: self.log.exception("An Error occurred while buying an offer") @@ -347,16 +401,21 @@ def _post_first_bid(self): if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.ONE_SIDED.value: return for market in self.active_markets: - if (self.state.can_buy_more_energy(market.time_slot) and - self._energy_params.allowed_operating_hours(market.time_slot) - and not self.are_bids_posted(market.id)): + if ( + self.state.can_buy_more_energy(market.time_slot) + and self._energy_params.allowed_operating_hours(market.time_slot) + and not self.are_bids_posted(market.id) + ): bid_energy = self.state.get_energy_requirement_Wh(market.time_slot) if self._is_eligible_for_balancing_market: - bid_energy -= (self.state.get_desired_energy_Wh(market.time_slot) * - self.balancing_energy_ratio.demand) + bid_energy -= ( + self.state.get_desired_energy_Wh(market.time_slot) + * self.balancing_energy_ratio.demand + ) try: - self.post_first_bid(market, bid_energy, - self.bid_update.initial_rate[market.time_slot]) + self.post_first_bid( + market, bid_energy, self.bid_update.initial_rate[market.time_slot] + ) except MarketException: pass @@ -380,7 +439,8 @@ def event_bid_traded(self, *, market_id, bid_trade): self._energy_params.decrement_energy_requirement( energy_kWh=bid_trade.traded_energy, time_slot=bid_trade.time_slot, - area_name=self.owner.name) + area_name=self.owner.name, + ) def event_offer_traded(self, *, market_id, trade): """Register the offer traded by the device and its effects. Extends the superclass method. @@ -406,19 +466,21 @@ def _demand_balancing_offer(self, market): if not self._is_eligible_for_balancing_market: return - ramp_up_energy = (self.balancing_energy_ratio.demand * - self.state.get_desired_energy_Wh(market.time_slot)) + ramp_up_energy = self.balancing_energy_ratio.demand * self.state.get_desired_energy_Wh( + market.time_slot + ) self._energy_params.decrement_energy_requirement( - energy_kWh=ramp_up_energy / 1000, - time_slot=market.time_slot, - area_name=self.owner.name) + energy_kWh=ramp_up_energy / 1000, time_slot=market.time_slot, area_name=self.owner.name + ) ramp_up_price = DeviceRegistry.REGISTRY[self.owner.name][0] * ramp_up_energy if ramp_up_energy != 0 and ramp_up_price != 0: self.area.get_balancing_market(market.time_slot).balancing_offer( - ramp_up_price, -ramp_up_energy, TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid)) + ramp_up_price, + -ramp_up_energy, + TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid), + ) # committing to reduce its consumption when required def _supply_balancing_offer(self, market, trade): @@ -429,8 +491,10 @@ def _supply_balancing_offer(self, market, trade): ramp_down_energy = self.balancing_energy_ratio.supply * trade.traded_energy ramp_down_price = DeviceRegistry.REGISTRY[self.owner.name][1] * ramp_down_energy self.area.get_balancing_market(market.time_slot).balancing_offer( - ramp_down_price, ramp_down_energy, TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid)) + ramp_down_price, + ramp_down_energy, + TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid), + ) @property def active_markets(self): @@ -442,16 +506,21 @@ def active_markets(self): return self._active_markets def _calculate_active_markets(self): - self._active_markets = [ - market for market in self.area.all_markets - if self._is_market_active(market) - ] if self.area else [] + self._active_markets = ( + [market for market in self.area.all_markets if self._is_market_active(market)] + if self.area + else [] + ) def _is_market_active(self, market): - return (self._energy_params.allowed_operating_hours(market.time_slot) and - market.in_sim_duration and - (not self.area.current_market or - market.time_slot >= self.area.current_market.time_slot)) + return ( + self._energy_params.allowed_operating_hours(market.time_slot) + and market.in_sim_duration + and ( + not self.area.current_market + or market.time_slot >= self.area.current_market.time_slot + ) + ) def _update_energy_requirement_future_markets(self): if not ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS: diff --git a/src/gsy_e/models/strategy/predefined_load.py b/src/gsy_e/models/strategy/predefined_load.py index 28d534aef0..8a137e4aee 100644 --- a/src/gsy_e/models/strategy/predefined_load.py +++ b/src/gsy_e/models/strategy/predefined_load.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 typing import Union from gsy_framework.constants_limits import ConstSettings @@ -26,23 +27,32 @@ class DefinedLoadStrategy(LoadHoursStrategy): """ - Strategy for creating a load profile. It accepts as an input a load csv file or a - dictionary that contains the load values for each time point + Strategy for creating a load profile. It accepts as an input a load csv file or a + dictionary that contains the load values for each time point """ + # pylint: disable=too-many-arguments - def __init__(self, daily_load_profile=None, - fit_to_limit=True, energy_rate_increase_per_update=None, - update_interval=None, - initial_buying_rate: Union[float, dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, - final_buying_rate: Union[float, dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, - balancing_energy_ratio: tuple = - (ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, - ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), - use_market_maker_rate: bool = False, - daily_load_profile_uuid: str = None, - daily_load_measurement_uuid: str = None): + def __init__( + self, + daily_load_profile=None, + fit_to_limit=True, + energy_rate_increase_per_update=None, + update_interval=None, + initial_buying_rate: Union[ + float, dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, + final_buying_rate: Union[ + float, dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, + balancing_energy_ratio: tuple = ( + ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, + ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO, + ), + use_market_maker_rate: bool = False, + daily_load_profile_uuid: str = None, + daily_load_measurement_uuid: str = None, + **kwargs + ): """ Constructor of DefinedLoadStrategy :param daily_load_profile: input profile for a day. Can be either a csv file path, @@ -58,20 +68,28 @@ def __init__(self, daily_load_profile=None, :param use_market_maker_rate: If set to True, Load would track its final buying rate as per utility's trading rate """ + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") + if update_interval is None: - update_interval = \ - duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) - - super().__init__(avg_power_W=0, hrs_of_day=list(range(0, 24)), - fit_to_limit=fit_to_limit, - energy_rate_increase_per_update=energy_rate_increase_per_update, - update_interval=update_interval, - final_buying_rate=final_buying_rate, - initial_buying_rate=initial_buying_rate, - balancing_energy_ratio=balancing_energy_ratio, - use_market_maker_rate=use_market_maker_rate) + update_interval = duration( + minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL + ) + + super().__init__( + avg_power_W=0, + hrs_of_day=list(range(0, 24)), + fit_to_limit=fit_to_limit, + energy_rate_increase_per_update=energy_rate_increase_per_update, + update_interval=update_interval, + final_buying_rate=final_buying_rate, + initial_buying_rate=initial_buying_rate, + balancing_energy_ratio=balancing_energy_ratio, + use_market_maker_rate=use_market_maker_rate, + ) self._energy_params = DefinedLoadEnergyParameters( - daily_load_profile, daily_load_profile_uuid, daily_load_measurement_uuid) + daily_load_profile, daily_load_profile_uuid, daily_load_measurement_uuid + ) # needed for profile_handler self.daily_load_profile_uuid = daily_load_profile_uuid diff --git a/src/gsy_e/models/strategy/predefined_pv.py b/src/gsy_e/models/strategy/predefined_pv.py index b33749724f..b938080af9 100644 --- a/src/gsy_e/models/strategy/predefined_pv.py +++ b/src/gsy_e/models/strategy/predefined_pv.py @@ -20,26 +20,31 @@ from pendulum import duration from gsy_e.models.strategy.energy_parameters.pv import ( - PVPredefinedEnergyParameters, PVUserProfileEnergyParameters) + PVPredefinedEnergyParameters, + PVUserProfileEnergyParameters, +) from gsy_e.models.strategy.pv import PVStrategy class PVPredefinedStrategy(PVStrategy): """ - Strategy responsible for using one of the predefined PV profiles. + Strategy responsible for using one of the predefined PV profiles. """ + # pylint: disable=too-many-arguments def __init__( - self, panel_count: int = 1, - initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - final_selling_rate: float = ConstSettings.PVSettings.SELLING_RATE_RANGE.final, - cloud_coverage: int = None, - fit_to_limit: bool = True, - update_interval=None, - energy_rate_decrease_per_update=None, - use_market_maker_rate: bool = False, - capacity_kW: float = None, - ): + self, + panel_count: int = 1, + initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + final_selling_rate: float = ConstSettings.PVSettings.SELLING_RATE_RANGE.final, + cloud_coverage: int = None, + fit_to_limit: bool = True, + update_interval=None, + energy_rate_decrease_per_update=None, + use_market_maker_rate: bool = False, + capacity_kW: float = None, + **kwargs + ): """ Constructor of PVPredefinedStrategy Args: @@ -57,23 +62,28 @@ def __init__( energy_rate_decrease_per_update: Slope of PV Offer change per update capacity_kW: power rating of the predefined profiles """ + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") if update_interval is None: - update_interval = \ - duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) - - super().__init__(panel_count=panel_count, - initial_selling_rate=initial_selling_rate, - final_selling_rate=final_selling_rate, - fit_to_limit=fit_to_limit, - update_interval=update_interval, - energy_rate_decrease_per_update=energy_rate_decrease_per_update, - capacity_kW=capacity_kW, - use_market_maker_rate=use_market_maker_rate - ) + update_interval = duration( + minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL + ) + + super().__init__( + panel_count=panel_count, + initial_selling_rate=initial_selling_rate, + final_selling_rate=final_selling_rate, + fit_to_limit=fit_to_limit, + update_interval=update_interval, + energy_rate_decrease_per_update=energy_rate_decrease_per_update, + capacity_kW=capacity_kW, + use_market_maker_rate=use_market_maker_rate, + ) self._energy_params = PVPredefinedEnergyParameters( - panel_count, cloud_coverage, capacity_kW) + panel_count, cloud_coverage, capacity_kW + ) def read_config_event(self): # this is to trigger to read from self.simulation_config.cloud_coverage: @@ -102,20 +112,23 @@ def area_reconfigure_event(self, *args, **kwargs): class PVUserProfileStrategy(PVStrategy): """ - Strategy responsible for reading a profile in the form of a dict of values. + Strategy responsible for reading a profile in the form of a dict of values. """ + # pylint: disable=too-many-arguments def __init__( - self, power_profile=None, panel_count: int = 1, - initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - final_selling_rate: float = ConstSettings.PVSettings.SELLING_RATE_RANGE.final, - fit_to_limit: bool = True, - update_interval=duration( - minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), - energy_rate_decrease_per_update=None, - use_market_maker_rate: bool = False, - power_profile_uuid: str = None, - power_measurement_uuid: str = None + self, + power_profile=None, + panel_count: int = 1, + initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + final_selling_rate: float = ConstSettings.PVSettings.SELLING_RATE_RANGE.final, + fit_to_limit: bool = True, + update_interval=duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), + energy_rate_decrease_per_update=None, + use_market_maker_rate: bool = False, + power_profile_uuid: str = None, + power_measurement_uuid: str = None, + **kwargs ): """ Constructor of PVUserProfileStrategy @@ -126,15 +139,21 @@ def __init__( panel_count: number of solar panels for this PV plant final_selling_rate: lower threshold for the PV sale price """ - super().__init__(panel_count=panel_count, - initial_selling_rate=initial_selling_rate, - final_selling_rate=final_selling_rate, - fit_to_limit=fit_to_limit, - update_interval=update_interval, - energy_rate_decrease_per_update=energy_rate_decrease_per_update, - use_market_maker_rate=use_market_maker_rate) + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") + + super().__init__( + panel_count=panel_count, + initial_selling_rate=initial_selling_rate, + final_selling_rate=final_selling_rate, + fit_to_limit=fit_to_limit, + update_interval=update_interval, + energy_rate_decrease_per_update=energy_rate_decrease_per_update, + use_market_maker_rate=use_market_maker_rate, + ) self._energy_params = PVUserProfileEnergyParameters( - panel_count, power_profile, power_profile_uuid, power_measurement_uuid) + panel_count, power_profile, power_profile_uuid, power_measurement_uuid + ) # needed for profile_handler self.power_profile_uuid = power_profile_uuid diff --git a/src/gsy_e/models/strategy/predefined_wind.py b/src/gsy_e/models/strategy/predefined_wind.py index 986d6287dd..5b4d56b9d9 100644 --- a/src/gsy_e/models/strategy/predefined_wind.py +++ b/src/gsy_e/models/strategy/predefined_wind.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 gsy_framework.constants_limits import ConstSettings from pendulum import duration @@ -23,20 +24,28 @@ class WindUserProfileStrategy(PVUserProfileStrategy): """Strategy for a wind turbine creating energy following the power_profile""" + # pylint: disable=too-many-arguments - def __init__(self, power_profile=None, - initial_selling_rate: - float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - final_selling_rate: float = ConstSettings.WindSettings.FINAL_SELLING_RATE, - fit_to_limit: bool = True, - update_interval=duration( - minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), - energy_rate_decrease_per_update=None, - power_profile_uuid: str = None - ): - super().__init__(power_profile=power_profile, initial_selling_rate=initial_selling_rate, - final_selling_rate=final_selling_rate, - fit_to_limit=fit_to_limit, update_interval=update_interval, - energy_rate_decrease_per_update=energy_rate_decrease_per_update, - power_profile_uuid=power_profile_uuid - ) + def __init__( + self, + power_profile=None, + initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + final_selling_rate: float = ConstSettings.WindSettings.FINAL_SELLING_RATE, + fit_to_limit: bool = True, + update_interval=duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), + energy_rate_decrease_per_update=None, + power_profile_uuid: str = None, + **kwargs + ): + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") + + super().__init__( + power_profile=power_profile, + initial_selling_rate=initial_selling_rate, + final_selling_rate=final_selling_rate, + fit_to_limit=fit_to_limit, + update_interval=update_interval, + energy_rate_decrease_per_update=energy_rate_decrease_per_update, + power_profile_uuid=power_profile_uuid, + ) diff --git a/src/gsy_e/models/strategy/pv.py b/src/gsy_e/models/strategy/pv.py index c0caee30ec..d8d72dca5e 100644 --- a/src/gsy_e/models/strategy/pv.py +++ b/src/gsy_e/models/strategy/pv.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import traceback from logging import getLogger @@ -22,8 +23,7 @@ from gsy_framework.data_classes import TraderDetails from gsy_framework.exceptions import GSyException from gsy_framework.read_user_profile import read_arbitrary_profile, InputProfileTypes -from gsy_framework.utils import ( - get_from_profile_same_weekday_and_time, key_in_dict_and_not_none) +from gsy_framework.utils import get_from_profile_same_weekday_and_time, key_in_dict_and_not_none from gsy_framework.validators import PVValidator from pendulum import duration @@ -45,16 +45,18 @@ class PVStrategy(BidEnabledStrategy, UseMarketMakerMixin): """PV Strategy class for gaussian generation profile.""" # pylint: disable=too-many-arguments - def __init__(self, panel_count: int = 1, - initial_selling_rate: - float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - final_selling_rate: - float = ConstSettings.PVSettings.SELLING_RATE_RANGE.final, - fit_to_limit: bool = True, - update_interval=None, - energy_rate_decrease_per_update=None, - capacity_kW: float = None, - use_market_maker_rate: bool = False): + def __init__( + self, + panel_count: int = 1, + initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + final_selling_rate: float = ConstSettings.PVSettings.SELLING_RATE_RANGE.final, + fit_to_limit: bool = True, + update_interval=None, + energy_rate_decrease_per_update=None, + capacity_kW: float = None, + use_market_maker_rate: bool = False, + **kwargs + ): """ Args: panel_count: Number of solar panels for this PV plant @@ -65,18 +67,26 @@ def __init__(self, panel_count: int = 1, energy_rate_decrease_per_update: Slope of PV Offer change per update capacity_kW: power rating of the predefined profiles """ + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") + super().__init__() self._energy_params = PVEnergyParameters(panel_count, capacity_kW) self.use_market_maker_rate = use_market_maker_rate - self._init_price_update(update_interval, initial_selling_rate, final_selling_rate, - fit_to_limit, energy_rate_decrease_per_update) + self._init_price_update( + update_interval, + initial_selling_rate, + final_selling_rate, + fit_to_limit, + energy_rate_decrease_per_update, + ) def serialize(self): """Return dict with the current strategy parameter values.""" return { **self._energy_params.serialize(), **self.offer_update.serialize(), - "use_market_maker_rate": self.use_market_maker_rate + "use_market_maker_rate": self.use_market_maker_rate, } @classmethod @@ -91,27 +101,38 @@ def state(self) -> PVState: return self._energy_params._state # pylint: disable=protected-access # pylint: disable=too-many-arguments - def _init_price_update(self, update_interval, initial_selling_rate, final_selling_rate, - fit_to_limit, energy_rate_decrease_per_update): + def _init_price_update( + self, + update_interval, + initial_selling_rate, + final_selling_rate, + fit_to_limit, + energy_rate_decrease_per_update, + ): # Instantiate instance variables that should not be shared with child classes self.final_selling_rate = final_selling_rate if update_interval is None: - update_interval = \ - duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) + update_interval = duration( + minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL + ) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) PVValidator.validate_rate( fit_to_limit=fit_to_limit, - energy_rate_decrease_per_update=energy_rate_decrease_per_update) + energy_rate_decrease_per_update=energy_rate_decrease_per_update, + ) - self.offer_update = TemplateStrategyOfferUpdater(initial_selling_rate, final_selling_rate, - fit_to_limit, - energy_rate_decrease_per_update, - update_interval) + self.offer_update = TemplateStrategyOfferUpdater( + initial_selling_rate, + final_selling_rate, + fit_to_limit, + energy_rate_decrease_per_update, + update_interval, + ) def area_reconfigure_event(self, *args, **kwargs): """Reconfigure the device properties at runtime using the provided arguments.""" @@ -125,23 +146,28 @@ def _area_reconfigure_prices(self, **kwargs): initial_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["initial_selling_rate"]) if kwargs.get("initial_selling_rate") is not None - else self.offer_update.initial_rate_profile_buffer) + else self.offer_update.initial_rate_profile_buffer + ) final_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["final_selling_rate"]) if kwargs.get("final_selling_rate") is not None - else self.offer_update.final_rate_profile_buffer) + else self.offer_update.final_rate_profile_buffer + ) energy_rate_change_per_update = ( - read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["energy_rate_decrease_per_update"]) + read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["energy_rate_decrease_per_update"] + ) if kwargs.get("energy_rate_decrease_per_update") is not None - else self.offer_update.energy_rate_change_per_update_profile_buffer) + else self.offer_update.energy_rate_change_per_update_profile_buffer + ) fit_to_limit = ( kwargs["fit_to_limit"] if kwargs.get("fit_to_limit") is not None - else self.offer_update.fit_to_limit) + else self.offer_update.fit_to_limit + ) if key_in_dict_and_not_none(kwargs, "update_interval"): if isinstance(kwargs["update_interval"], int): @@ -155,11 +181,15 @@ def _area_reconfigure_prices(self, **kwargs): self.use_market_maker_rate = kwargs["use_market_maker_rate"] try: - self._validate_rates(initial_rate, final_rate, energy_rate_change_per_update, - fit_to_limit) + self._validate_rates( + initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ) except GSyException as e: # pylint: disable=broad-except - log.error("PVStrategy._area_reconfigure_prices failed. Exception: %s. " - "Traceback: %s", e, traceback.format_exc()) + log.error( + "PVStrategy._area_reconfigure_prices failed. Exception: %s. Traceback: %s", + e, + traceback.format_exc(), + ) return self.offer_update.set_parameters( @@ -167,23 +197,33 @@ def _area_reconfigure_prices(self, **kwargs): final_rate=final_rate, energy_rate_change_per_update=energy_rate_change_per_update, fit_to_limit=fit_to_limit, - update_interval=update_interval + update_interval=update_interval, ) - def _validate_rates(self, initial_rate, final_rate, energy_rate_change_per_update, - fit_to_limit): + def _validate_rates( + self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ): # all parameters have to be validated for each time slot here for time_slot in initial_rate.keys(): - if self.area and self.area.current_market \ - and time_slot < self.area.current_market.time_slot: + if ( + self.area + and self.area.current_market + and time_slot < self.area.current_market.time_slot + ): continue - rate_change = None if fit_to_limit else \ - get_from_profile_same_weekday_and_time(energy_rate_change_per_update, time_slot) + rate_change = ( + None + if fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_change_per_update, time_slot + ) + ) PVValidator.validate_rate( initial_selling_rate=initial_rate[time_slot], final_selling_rate=get_from_profile_same_weekday_and_time(final_rate, time_slot), energy_rate_decrease_per_update=rate_change, - fit_to_limit=fit_to_limit) + fit_to_limit=fit_to_limit, + ) def event_activate(self, **kwargs): self.event_activate_price() @@ -194,10 +234,12 @@ def event_activate(self, **kwargs): def event_activate_price(self): self._replace_rates_with_market_maker_rates() - self._validate_rates(self.offer_update.initial_rate_profile_buffer, - self.offer_update.final_rate_profile_buffer, - self.offer_update.energy_rate_change_per_update_profile_buffer, - self.offer_update.fit_to_limit) + self._validate_rates( + self.offer_update.initial_rate_profile_buffer, + self.offer_update.final_rate_profile_buffer, + self.offer_update.energy_rate_change_per_update_profile_buffer, + self.offer_update.fit_to_limit, + ) def event_activate_energy(self): """Activate energy parameters of the PV.""" @@ -228,7 +270,8 @@ def set_produced_energy_forecast_in_state(self, reconfigure=True): time_slots.extend(self.area.future_market_time_slots) for time_slot in time_slots: self._energy_params.set_produced_energy_forecast( - time_slot, self.simulation_config.slot_length) + time_slot, self.simulation_config.slot_length + ) def event_market_cycle(self): super().event_market_cycle() @@ -246,14 +289,15 @@ def _set_energy_measurement_of_last_market(self): self._energy_params.set_energy_measurement_kWh(self.area.current_market.time_slot) def _delete_past_state(self): - if (constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True or - self.area.current_market is None): + if ( + constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True + or self.area.current_market is None + ): return self.state.delete_past_state_values(self.area.current_market.time_slot) self.offer_update.delete_past_state_values(self.area.current_market.time_slot) - self._future_market_strategy.delete_past_state_values( - self.area.current_market.time_slot) + self._future_market_strategy.delete_past_state_values(self.area.current_market.time_slot) def event_market_cycle_price(self): """Manage price parameters during the market cycle event.""" @@ -266,16 +310,16 @@ def event_market_cycle_price(self): # market in order to validate that more offers need to be posted. offer_energy_kWh -= self.offers.open_offer_energy(market.id) if offer_energy_kWh > 0: - offer_price = \ - self.offer_update.initial_rate[market.time_slot] * offer_energy_kWh + offer_price = self.offer_update.initial_rate[market.time_slot] * offer_energy_kWh try: offer = market.offer( offer_price, offer_energy_kWh, - TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, - self.owner.uuid), + TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), original_price=offer_price, - time_slot=market.time_slot + time_slot=market.time_slot, ) self.offers.post(offer, market.id) except MarketException: @@ -292,7 +336,8 @@ def event_offer_traded(self, *, market_id, trade): if trade.seller.name == self.owner.name: self.state.decrement_available_energy( - trade.traded_energy, trade.time_slot, self.owner.name) + trade.traded_energy, trade.time_slot, self.owner.name + ) def event_bid_traded(self, *, market_id, bid_trade): super().event_bid_traded(market_id=market_id, bid_trade=bid_trade) diff --git a/src/gsy_e/models/strategy/smart_meter.py b/src/gsy_e/models/strategy/smart_meter.py index 4ca699126c..5d59d0e4fd 100644 --- a/src/gsy_e/models/strategy/smart_meter.py +++ b/src/gsy_e/models/strategy/smart_meter.py @@ -35,8 +35,10 @@ from gsy_e.models.strategy.energy_parameters.smart_meter import SmartMeterEnergyParameters from gsy_e.models.strategy.mixins import UseMarketMakerMixin from gsy_e.models.strategy.state import SmartMeterState -from gsy_e.models.strategy.update_frequency import (TemplateStrategyBidUpdater, - TemplateStrategyOfferUpdater) +from gsy_e.models.strategy.update_frequency import ( + TemplateStrategyBidUpdater, + TemplateStrategyOfferUpdater, +) log = getLogger(__name__) @@ -52,25 +54,25 @@ def serialize(self): **self.offer_update.serialize(), # Price consumption parameters **self.bid_update.serialize(), - "use_market_maker_rate": self.use_market_maker_rate + "use_market_maker_rate": self.use_market_maker_rate, } # pylint: disable=too-many-arguments def __init__( - self, - smart_meter_profile: Union[Path, str, Dict[int, float], Dict[str, float]] = None, - initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - final_selling_rate: float = ConstSettings.SmartMeterSettings.SELLING_RATE_RANGE.final, - energy_rate_decrease_per_update: Union[float, None] = None, - initial_buying_rate: float = ( - ConstSettings.SmartMeterSettings.BUYING_RATE_RANGE.initial), - final_buying_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - energy_rate_increase_per_update: Union[float, None] = None, - fit_to_limit: bool = True, - update_interval=None, - use_market_maker_rate: bool = False, - smart_meter_profile_uuid: str = None, - smart_meter_measurement_uuid: str = None + self, + smart_meter_profile: Union[Path, str, Dict[int, float], Dict[str, float]] = None, + initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + final_selling_rate: float = ConstSettings.SmartMeterSettings.SELLING_RATE_RANGE.final, + energy_rate_decrease_per_update: Union[float, None] = None, + initial_buying_rate: float = (ConstSettings.SmartMeterSettings.BUYING_RATE_RANGE.initial), + final_buying_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + energy_rate_increase_per_update: Union[float, None] = None, + fit_to_limit: bool = True, + update_interval=None, + use_market_maker_rate: bool = False, + smart_meter_profile_uuid: str = None, + smart_meter_measurement_uuid: str = None, + **kwargs ): """ Args: @@ -95,10 +97,14 @@ def __init__( use_market_maker_rate: If set to True, the Smart Meter will track its final buying and selling rate as per utility's trading rate. """ + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") + super().__init__() self._energy_params = SmartMeterEnergyParameters( - smart_meter_profile, smart_meter_profile_uuid, smart_meter_measurement_uuid) + smart_meter_profile, smart_meter_profile_uuid, smart_meter_measurement_uuid + ) # needed for profile_handler self.smart_meter_profile_uuid = smart_meter_profile_uuid @@ -111,7 +117,8 @@ def __init__( self.validator.validate( fit_to_limit=fit_to_limit, energy_rate_increase_per_update=energy_rate_increase_per_update, - energy_rate_decrease_per_update=energy_rate_decrease_per_update) + energy_rate_decrease_per_update=energy_rate_decrease_per_update, + ) # Instances to update the Smart Meter's bids and offers across all market slots self.bid_update = TemplateStrategyBidUpdater( @@ -120,7 +127,8 @@ def __init__( fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, - rate_limit_object=min) + rate_limit_object=min, + ) self.offer_update = TemplateStrategyOfferUpdater( initial_rate=initial_selling_rate, @@ -128,7 +136,8 @@ def __init__( fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_decrease_per_update, update_interval=update_interval, - rate_limit_object=max) + rate_limit_object=max, + ) @property def state(self) -> SmartMeterState: @@ -150,15 +159,19 @@ def event_activate_price(self): initial_rate=self.bid_update.initial_rate_profile_buffer, final_rate=self.bid_update.final_rate_profile_buffer, energy_rate_change_per_update=( - self.bid_update.energy_rate_change_per_update_profile_buffer), - fit_to_limit=self.bid_update.fit_to_limit) + self.bid_update.energy_rate_change_per_update_profile_buffer + ), + fit_to_limit=self.bid_update.fit_to_limit, + ) self._validate_production_rates( initial_rate=self.offer_update.initial_rate_profile_buffer, final_rate=self.offer_update.final_rate_profile_buffer, energy_rate_change_per_update=( - self.offer_update.energy_rate_change_per_update_profile_buffer), - fit_to_limit=self.offer_update.fit_to_limit) + self.offer_update.energy_rate_change_per_update_profile_buffer + ), + fit_to_limit=self.offer_update.fit_to_limit, + ) def event_activate_energy(self): """Read the power profile and update the energy requirements for future market slots. @@ -167,8 +180,7 @@ def event_activate_energy(self): """ self._energy_params.activate(self.owner) time_slots = [m.time_slot for m in self.area.all_markets] - self._energy_params.set_energy_forecast_for_future_markets( - time_slots, reconfigure=True) + self._energy_params.set_energy_forecast_for_future_markets(time_slots, reconfigure=True) def event_market_cycle(self): """Prepare rates and execute bids/offers when a new market slot begins. @@ -179,8 +191,7 @@ def event_market_cycle(self): self._energy_params.read_and_rotate_profiles() self._reset_rates_and_update_prices() time_slots = [m.time_slot for m in self.area.all_markets] - self._energy_params.set_energy_forecast_for_future_markets( - time_slots, reconfigure=False) + self._energy_params.set_energy_forecast_for_future_markets(time_slots, reconfigure=False) self._set_energy_measurement_of_last_market() # Create bids/offers for the expected energy consumption/production in future markets for market in self.area.all_markets: @@ -241,7 +252,8 @@ def event_offer_traded(self, *, market_id, trade): else: self._assert_if_trade_offer_price_is_too_low(market_id, trade) self.state.decrement_available_energy( - trade.traded_energy, market.time_slot, self.owner.name) + trade.traded_energy, market.time_slot, self.owner.name + ) def event_bid_traded(self, *, market_id, bid_trade): """Register the bid traded by the device. Extends the superclass method. @@ -257,7 +269,8 @@ def event_bid_traded(self, *, market_id, bid_trade): self._energy_params.decrement_energy_requirement( energy_kWh=bid_trade.traded_energy, time_slot=market.time_slot, - area_name=self.owner.name) + area_name=self.owner.name, + ) def area_reconfigure_event(self, *args, **kwargs): """Reconfigure the device properties at runtime using the provided arguments. @@ -278,23 +291,28 @@ def _area_reconfigure_production_prices(self, **kwargs): initial_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["initial_selling_rate"]) if kwargs.get("initial_selling_rate") is not None - else self.offer_update.initial_rate_profile_buffer) + else self.offer_update.initial_rate_profile_buffer + ) final_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["final_selling_rate"]) if kwargs.get("final_selling_rate") is not None - else self.offer_update.final_rate_profile_buffer) + else self.offer_update.final_rate_profile_buffer + ) energy_rate_change_per_update = ( - read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["energy_rate_decrease_per_update"]) + read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["energy_rate_decrease_per_update"] + ) if kwargs.get("energy_rate_decrease_per_update") is not None - else self.offer_update.energy_rate_change_per_update_profile_buffer) + else self.offer_update.energy_rate_change_per_update_profile_buffer + ) fit_to_limit = ( kwargs["fit_to_limit"] if kwargs.get("fit_to_limit") is not None - else self.offer_update.fit_to_limit) + else self.offer_update.fit_to_limit + ) if kwargs.get("update_interval") is not None: if isinstance(kwargs["update_interval"], int): @@ -309,7 +327,8 @@ def _area_reconfigure_production_prices(self, **kwargs): try: self._validate_production_rates( - initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit) + initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ) except GSyException as ex: log.exception("SmartMeterStrategy._area_reconfigure_production_prices failed: %s", ex) return @@ -319,29 +338,35 @@ def _area_reconfigure_production_prices(self, **kwargs): final_rate=final_rate, energy_rate_change_per_update=energy_rate_change_per_update, fit_to_limit=fit_to_limit, - update_interval=update_interval) + update_interval=update_interval, + ) def _area_reconfigure_consumption_prices(self, **kwargs): initial_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["initial_buying_rate"]) if kwargs.get("initial_buying_rate") is not None - else self.bid_update.initial_rate_profile_buffer) + else self.bid_update.initial_rate_profile_buffer + ) final_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["final_buying_rate"]) if kwargs.get("final_buying_rate") is not None - else self.bid_update.final_rate_profile_buffer) + else self.bid_update.final_rate_profile_buffer + ) energy_rate_change_per_update = ( - read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["energy_rate_increase_per_update"]) + read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["energy_rate_increase_per_update"] + ) if kwargs.get("energy_rate_increase_per_update") is not None - else self.bid_update.energy_rate_change_per_update_profile_buffer) + else self.bid_update.energy_rate_change_per_update_profile_buffer + ) fit_to_limit = ( kwargs["fit_to_limit"] if kwargs.get("fit_to_limit") is not None - else self.bid_update.fit_to_limit) + else self.bid_update.fit_to_limit + ) if kwargs.get("update_interval") is not None: if isinstance(kwargs["update_interval"], int): @@ -356,7 +381,8 @@ def _area_reconfigure_consumption_prices(self, **kwargs): try: self._validate_consumption_rates( - initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit) + initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ) except GSyException as ex: log.exception(ex) return @@ -366,7 +392,8 @@ def _area_reconfigure_consumption_prices(self, **kwargs): final_rate=final_rate, energy_rate_change_per_update=energy_rate_change_per_update, fit_to_limit=fit_to_limit, - update_interval=update_interval) + update_interval=update_interval, + ) def _reset_rates_and_update_prices(self): """Set the initial/final rates and update the price of all bids/offers consequently.""" @@ -386,9 +413,11 @@ def _post_offer(self, market): offer = market.offer( offer_price, offer_energy_kWh, - TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, - self.owner.uuid), - original_price=offer_price) + TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), + original_price=offer_price, + ) self.offers.post(offer, market.id) except MarketException: pass @@ -404,8 +433,9 @@ def _post_first_bid(self, market): # self.balancing_energy_ratio.demand try: if not self.are_bids_posted(market.id): - self.post_first_bid(market, bid_energy, - self.bid_update.initial_rate[market.time_slot]) + self.post_first_bid( + market, bid_energy, self.bid_update.initial_rate[market.time_slot] + ) except MarketException: pass @@ -420,8 +450,10 @@ def _convert_update_interval_to_duration(update_interval): return None def _delete_past_state(self): - if (constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True or - self.area.current_market is None): + if ( + constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True + or self.area.current_market is None + ): return # Delete past energy requirements and availability @@ -431,39 +463,53 @@ def _delete_past_state(self): # Delete offer rates for previous market slots self.offer_update.delete_past_state_values(self.area.current_market.time_slot) # Delete the state of the current slot from the future market cache - self._future_market_strategy.delete_past_state_values( - self.area.current_market.time_slot) + self._future_market_strategy.delete_past_state_values(self.area.current_market.time_slot) def _validate_consumption_rates( - self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit): + self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ): for time_slot in initial_rate.keys(): - rate_change = None if fit_to_limit else get_from_profile_same_weekday_and_time( - energy_rate_change_per_update, time_slot) + rate_change = ( + None + if fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_change_per_update, time_slot + ) + ) self.validator.validate_rate( initial_buying_rate=initial_rate[time_slot], energy_rate_increase_per_update=rate_change, final_buying_rate=get_from_profile_same_weekday_and_time(final_rate, time_slot), - fit_to_limit=fit_to_limit) + fit_to_limit=fit_to_limit, + ) def _validate_production_rates( - self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit): + self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ): for time_slot in initial_rate.keys(): - rate_change = None if fit_to_limit else get_from_profile_same_weekday_and_time( - energy_rate_change_per_update, time_slot) + rate_change = ( + None + if fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_change_per_update, time_slot + ) + ) self.validator.validate_rate( initial_selling_rate=initial_rate[time_slot], final_selling_rate=get_from_profile_same_weekday_and_time(final_rate, time_slot), energy_rate_decrease_per_update=rate_change, - fit_to_limit=fit_to_limit) + fit_to_limit=fit_to_limit, + ) def _offer_rate_can_be_accepted(self, offer: Offer, market_slot: MarketBase): """Check if the offer rate is less than what the device wants to pay.""" max_affordable_offer_rate = self.bid_update.get_updated_rate(market_slot.time_slot) return ( limit_float_precision(offer.energy_rate) - <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE) + <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE + ) def _event_tick_consumption(self): for market in self.area.all_markets: @@ -500,15 +546,20 @@ def _one_sided_market_event_tick(self, market, offer=None): if acceptable_offer and self._offer_rate_can_be_accepted(acceptable_offer, market): # If the device can still buy more energy energy_Wh = self.state.calculate_energy_to_accept( - acceptable_offer.energy * 1000.0, time_slot) - self.accept_offer(market, acceptable_offer, buyer=TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid - ), energy=energy_Wh / 1000.0) + acceptable_offer.energy * 1000.0, time_slot + ) + self.accept_offer( + market, + acceptable_offer, + buyer=TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), + energy=energy_Wh / 1000.0, + ) self._energy_params.decrement_energy_requirement( - energy_kWh=energy_Wh / 1000, - time_slot=time_slot, - area_name=self.owner.name) + energy_kWh=energy_Wh / 1000, time_slot=time_slot, area_name=self.owner.name + ) except MarketException: self.log.exception("An Error occurred while buying an offer.") diff --git a/src/gsy_e/models/strategy/storage.py b/src/gsy_e/models/strategy/storage.py index 51927cc5e6..1529dec631 100644 --- a/src/gsy_e/models/strategy/storage.py +++ b/src/gsy_e/models/strategy/storage.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 collections import namedtuple from enum import Enum from logging import getLogger @@ -25,8 +26,7 @@ from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.exceptions import GSyException from gsy_framework.read_user_profile import InputProfileTypes, read_arbitrary_profile -from gsy_framework.utils import ( - get_from_profile_same_weekday_and_time, key_in_dict_and_not_none) +from gsy_framework.utils import get_from_profile_same_weekday_and_time, key_in_dict_and_not_none from gsy_framework.validators import StorageValidator from pendulum import duration @@ -39,7 +39,9 @@ from gsy_e.models.strategy import BidEnabledStrategy from gsy_e.models.strategy.future.strategy import future_market_strategy_factory from gsy_e.models.strategy.update_frequency import ( - TemplateStrategyBidUpdater, TemplateStrategyOfferUpdater) + TemplateStrategyBidUpdater, + TemplateStrategyOfferUpdater, +) log = getLogger(__name__) @@ -67,41 +69,49 @@ def serialize(self): } def __init__( # pylint: disable=too-many-arguments, too-many-locals - self, initial_soc: float = StorageSettings.MIN_ALLOWED_SOC, + self, + initial_soc: float = StorageSettings.MIN_ALLOWED_SOC, min_allowed_soc=StorageSettings.MIN_ALLOWED_SOC, battery_capacity_kWh: float = StorageSettings.CAPACITY, max_abs_battery_power_kW: float = StorageSettings.MAX_ABS_POWER, cap_price_strategy: bool = False, - initial_selling_rate: Union[float, dict] = - StorageSettings.SELLING_RATE_RANGE.initial, - final_selling_rate: Union[float, dict] = - StorageSettings.SELLING_RATE_RANGE.final, - initial_buying_rate: Union[float, dict] = - StorageSettings.BUYING_RATE_RANGE.initial, - final_buying_rate: Union[float, dict] = - StorageSettings.BUYING_RATE_RANGE.final, - fit_to_limit=True, energy_rate_increase_per_update=None, + initial_selling_rate: Union[float, dict] = StorageSettings.SELLING_RATE_RANGE.initial, + final_selling_rate: Union[float, dict] = StorageSettings.SELLING_RATE_RANGE.final, + initial_buying_rate: Union[float, dict] = StorageSettings.BUYING_RATE_RANGE.initial, + final_buying_rate: Union[float, dict] = StorageSettings.BUYING_RATE_RANGE.final, + fit_to_limit=True, + energy_rate_increase_per_update=None, energy_rate_decrease_per_update=None, update_interval=None, initial_energy_origin: Enum = ESSEnergyOrigin.EXTERNAL, balancing_energy_ratio: tuple = ( - BalancingSettings.OFFER_DEMAND_RATIO, BalancingSettings.OFFER_SUPPLY_RATIO)): + BalancingSettings.OFFER_DEMAND_RATIO, + BalancingSettings.OFFER_SUPPLY_RATIO, + ), + **kwargs + ): + + if kwargs.get("linear_pricing") is not None: + fit_to_limit = kwargs.get("linear_pricing") if update_interval is None: update_interval = duration( - minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) + minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL + ) if min_allowed_soc is None: min_allowed_soc = StorageSettings.MIN_ALLOWED_SOC self.initial_soc = initial_soc StorageValidator.validate( - initial_soc=initial_soc, min_allowed_soc=min_allowed_soc, + initial_soc=initial_soc, + min_allowed_soc=min_allowed_soc, battery_capacity_kWh=battery_capacity_kWh, max_abs_battery_power_kW=max_abs_battery_power_kW, fit_to_limit=fit_to_limit, energy_rate_increase_per_update=energy_rate_increase_per_update, - energy_rate_decrease_per_update=energy_rate_decrease_per_update) + energy_rate_decrease_per_update=energy_rate_decrease_per_update, + ) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) @@ -109,30 +119,41 @@ def __init__( # pylint: disable=too-many-arguments, too-many-locals super().__init__() self.offer_update = TemplateStrategyOfferUpdater( - initial_rate=initial_selling_rate, final_rate=final_selling_rate, + initial_rate=initial_selling_rate, + final_rate=final_selling_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_decrease_per_update, - update_interval=update_interval) + update_interval=update_interval, + ) for time_slot in self.offer_update.initial_rate_profile_buffer.keys(): StorageValidator.validate( initial_selling_rate=self.offer_update.initial_rate_profile_buffer[time_slot], final_selling_rate=get_from_profile_same_weekday_and_time( - self.offer_update.final_rate_profile_buffer, time_slot)) + self.offer_update.final_rate_profile_buffer, time_slot + ), + ) self.bid_update = TemplateStrategyBidUpdater( - initial_rate=initial_buying_rate, final_rate=final_buying_rate, + initial_rate=initial_buying_rate, + final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, - update_interval=update_interval, rate_limit_object=min + update_interval=update_interval, + rate_limit_object=min, ) for time_slot in self.bid_update.initial_rate_profile_buffer.keys(): StorageValidator.validate( initial_buying_rate=self.bid_update.initial_rate_profile_buffer[time_slot], final_buying_rate=get_from_profile_same_weekday_and_time( - self.bid_update.final_rate_profile_buffer, time_slot)) + self.bid_update.final_rate_profile_buffer, time_slot + ), + ) self._state = StorageState( - initial_soc=initial_soc, initial_energy_origin=initial_energy_origin, - capacity=battery_capacity_kWh, max_abs_battery_power_kW=max_abs_battery_power_kW, - min_allowed_soc=min_allowed_soc) + initial_soc=initial_soc, + initial_energy_origin=initial_energy_origin, + capacity=battery_capacity_kWh, + max_abs_battery_power_kW=max_abs_battery_power_kW, + min_allowed_soc=min_allowed_soc, + ) self.cap_price_strategy = cap_price_strategy self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio) @@ -145,37 +166,45 @@ def state(self) -> StorageState: def _area_reconfigure_prices(self, **kwargs): # pylint: disable=too-many-branches if key_in_dict_and_not_none(kwargs, "initial_selling_rate"): - initial_selling_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["initial_selling_rate"]) + initial_selling_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["initial_selling_rate"] + ) else: initial_selling_rate = self.offer_update.initial_rate_profile_buffer if key_in_dict_and_not_none(kwargs, "final_selling_rate"): - final_selling_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["final_selling_rate"]) + final_selling_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["final_selling_rate"] + ) else: final_selling_rate = self.offer_update.final_rate_profile_buffer if key_in_dict_and_not_none(kwargs, "initial_buying_rate"): - initial_buying_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["initial_buying_rate"]) + initial_buying_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["initial_buying_rate"] + ) else: initial_buying_rate = self.bid_update.initial_rate_profile_buffer if key_in_dict_and_not_none(kwargs, "final_buying_rate"): - final_buying_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["final_buying_rate"]) + final_buying_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["final_buying_rate"] + ) else: final_buying_rate = self.bid_update.final_rate_profile_buffer if key_in_dict_and_not_none(kwargs, "energy_rate_decrease_per_update"): energy_rate_decrease_per_update = read_arbitrary_profile( - InputProfileTypes.IDENTITY, kwargs["energy_rate_decrease_per_update"]) + InputProfileTypes.IDENTITY, kwargs["energy_rate_decrease_per_update"] + ) else: - energy_rate_decrease_per_update = (self.offer_update. - energy_rate_change_per_update_profile_buffer) + energy_rate_decrease_per_update = ( + self.offer_update.energy_rate_change_per_update_profile_buffer + ) if key_in_dict_and_not_none(kwargs, "energy_rate_increase_per_update"): energy_rate_increase_per_update = read_arbitrary_profile( - InputProfileTypes.IDENTITY, kwargs["energy_rate_increase_per_update"]) + InputProfileTypes.IDENTITY, kwargs["energy_rate_increase_per_update"] + ) else: energy_rate_increase_per_update = ( - self.bid_update.energy_rate_change_per_update_profile_buffer) + self.bid_update.energy_rate_change_per_update_profile_buffer + ) if key_in_dict_and_not_none(kwargs, "fit_to_limit"): bid_fit_to_limit = kwargs["fit_to_limit"] offer_fit_to_limit = kwargs["fit_to_limit"] @@ -191,10 +220,16 @@ def _area_reconfigure_prices(self, **kwargs): # pylint: disable=too-many-branch update_interval = self.bid_update.update_interval try: - self._validate_rates(initial_selling_rate, final_selling_rate, - initial_buying_rate, final_buying_rate, - energy_rate_increase_per_update, energy_rate_decrease_per_update, - bid_fit_to_limit, offer_fit_to_limit) + self._validate_rates( + initial_selling_rate, + final_selling_rate, + initial_buying_rate, + final_buying_rate, + energy_rate_increase_per_update, + energy_rate_decrease_per_update, + bid_fit_to_limit, + offer_fit_to_limit, + ) except GSyException as ex: log.exception("StorageStrategy._area_reconfigure_prices failed. Exception: %s.", ex) return @@ -204,14 +239,14 @@ def _area_reconfigure_prices(self, **kwargs): # pylint: disable=too-many-branch final_rate=final_selling_rate, energy_rate_change_per_update=energy_rate_decrease_per_update, fit_to_limit=offer_fit_to_limit, - update_interval=update_interval + update_interval=update_interval, ) self.bid_update.set_parameters( initial_rate=initial_buying_rate, final_rate=final_buying_rate, energy_rate_change_per_update=energy_rate_increase_per_update, fit_to_limit=bid_fit_to_limit, - update_interval=update_interval + update_interval=update_interval, ) def area_reconfigure_event(self, *args, **kwargs): @@ -220,52 +255,80 @@ def area_reconfigure_event(self, *args, **kwargs): self._update_profiles_with_default_values() def _validate_rates( # pylint: disable=too-many-arguments - self, initial_selling_rate, final_selling_rate, - initial_buying_rate, final_buying_rate, - energy_rate_increase_per_update, energy_rate_decrease_per_update, - bid_fit_to_limit, offer_fit_to_limit): + self, + initial_selling_rate, + final_selling_rate, + initial_buying_rate, + final_buying_rate, + energy_rate_increase_per_update, + energy_rate_decrease_per_update, + bid_fit_to_limit, + offer_fit_to_limit, + ): for time_slot in initial_selling_rate.keys(): - if self.area and \ - self.area.current_market and time_slot < self.area.current_market.time_slot: + if ( + self.area + and self.area.current_market + and time_slot < self.area.current_market.time_slot + ): continue - bid_rate_change = (None if bid_fit_to_limit else - get_from_profile_same_weekday_and_time( - energy_rate_increase_per_update, time_slot)) - offer_rate_change = (None if offer_fit_to_limit else - get_from_profile_same_weekday_and_time( - energy_rate_decrease_per_update, time_slot)) + bid_rate_change = ( + None + if bid_fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_increase_per_update, time_slot + ) + ) + offer_rate_change = ( + None + if offer_fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_decrease_per_update, time_slot + ) + ) StorageValidator.validate( initial_selling_rate=initial_selling_rate[time_slot], final_selling_rate=get_from_profile_same_weekday_and_time( - final_selling_rate, time_slot), + final_selling_rate, time_slot + ), initial_buying_rate=get_from_profile_same_weekday_and_time( - initial_buying_rate, time_slot), + initial_buying_rate, time_slot + ), final_buying_rate=get_from_profile_same_weekday_and_time( - final_buying_rate, time_slot), + final_buying_rate, time_slot + ), energy_rate_increase_per_update=bid_rate_change, - energy_rate_decrease_per_update=offer_rate_change) + energy_rate_decrease_per_update=offer_rate_change, + ) def event_on_disabled_area(self): self.state.check_state(self.area.spot_market.time_slot) def event_activate_price(self): """Validate rates of offers and bids when the ACTIVATE event is triggered.""" - self._validate_rates(self.offer_update.initial_rate_profile_buffer, - self.offer_update.final_rate_profile_buffer, - self.bid_update.initial_rate_profile_buffer, - self.bid_update.final_rate_profile_buffer, - self.bid_update.energy_rate_change_per_update_profile_buffer, - self.offer_update.energy_rate_change_per_update_profile_buffer, - self.bid_update.fit_to_limit, self.offer_update.fit_to_limit) + self._validate_rates( + self.offer_update.initial_rate_profile_buffer, + self.offer_update.final_rate_profile_buffer, + self.bid_update.initial_rate_profile_buffer, + self.bid_update.final_rate_profile_buffer, + self.bid_update.energy_rate_change_per_update_profile_buffer, + self.offer_update.energy_rate_change_per_update_profile_buffer, + self.bid_update.fit_to_limit, + self.offer_update.fit_to_limit, + ) def event_activate_energy(self): """Set the battery energy for each slot when the ACTIVATE event is triggered.""" self.state.activate( self.simulation_config.slot_length, - self.area.current_market.time_slot - if self.area.current_market else self.area.config.start_date) + ( + self.area.current_market.time_slot + if self.area.current_market + else self.area.config.start_date + ), + ) def event_activate(self, **kwargs): self._update_profiles_with_default_values() @@ -274,9 +337,16 @@ def event_activate(self, **kwargs): @staticmethod def _validate_constructor_arguments( # pylint: disable=too-many-arguments, too-many-branches - initial_soc=None, min_allowed_soc=None, battery_capacity_kWh=None, - max_abs_battery_power_kW=None, initial_selling_rate=None, final_selling_rate=None, - initial_buying_rate=None, final_buying_rate=None, energy_rate_change_per_update=None): + initial_soc=None, + min_allowed_soc=None, + battery_capacity_kWh=None, + max_abs_battery_power_kW=None, + initial_selling_rate=None, + final_selling_rate=None, + initial_buying_rate=None, + final_buying_rate=None, + energy_rate_change_per_update=None, + ): if battery_capacity_kWh is not None and battery_capacity_kWh < 0: raise ValueError("Battery capacity should be a positive integer") @@ -286,47 +356,64 @@ def _validate_constructor_arguments( # pylint: disable=too-many-arguments, too- raise ValueError("initial SOC must be in between 0-100 %") if min_allowed_soc is not None and 0 < min_allowed_soc > 100: raise ValueError("initial SOC must be in between 0-100 %") - if (initial_soc is not None and min_allowed_soc is not None and - initial_soc < min_allowed_soc): + if ( + initial_soc is not None + and min_allowed_soc is not None + and initial_soc < min_allowed_soc + ): raise ValueError("Initial charge must be more than the minimum allowed soc.") if initial_selling_rate is not None and initial_selling_rate < 0: raise ValueError("Initial selling rate must be greater equal 0.") if final_selling_rate is not None: if isinstance(final_selling_rate, float) and final_selling_rate < 0: raise ValueError("Final selling rate must be greater equal 0.") - if (isinstance(final_selling_rate, dict) and - any(rate < 0 for _, rate in final_selling_rate.items())): + if isinstance(final_selling_rate, dict) and any( + rate < 0 for _, rate in final_selling_rate.items() + ): raise ValueError("Final selling rate must be greater equal 0.") if initial_selling_rate is not None and final_selling_rate is not None: - initial_selling_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - initial_selling_rate) - final_selling_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - final_selling_rate) - if any(initial_selling_rate[hour] < final_selling_rate[hour] - for hour, _ in initial_selling_rate.items()): + initial_selling_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, initial_selling_rate + ) + final_selling_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, final_selling_rate + ) + if any( + initial_selling_rate[hour] < final_selling_rate[hour] + for hour, _ in initial_selling_rate.items() + ): raise ValueError("Initial selling rate must be greater than final selling rate.") if initial_buying_rate is not None and initial_buying_rate < 0: raise ValueError("Initial buying rate must be greater equal 0.") if final_buying_rate is not None: - final_buying_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - final_buying_rate) + final_buying_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, final_buying_rate + ) if any(rate < 0 for _, rate in final_buying_rate.items()): raise ValueError("Final buying rate must be greater equal 0.") if initial_buying_rate is not None and final_buying_rate is not None: - initial_buying_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - initial_buying_rate) - final_buying_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - final_buying_rate) - if any(initial_buying_rate[hour] > final_buying_rate[hour] - for hour, _ in initial_buying_rate.items()): + initial_buying_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, initial_buying_rate + ) + final_buying_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, final_buying_rate + ) + if any( + initial_buying_rate[hour] > final_buying_rate[hour] + for hour, _ in initial_buying_rate.items() + ): raise ValueError("Initial buying rate must be less than final buying rate.") if final_selling_rate is not None and final_buying_rate is not None: - final_selling_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - final_selling_rate) - final_buying_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - final_buying_rate) - if any(final_buying_rate[hour] >= final_selling_rate[hour] - for hour, _ in final_selling_rate.items()): + final_selling_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, final_selling_rate + ) + final_buying_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, final_buying_rate + ) + if any( + final_buying_rate[hour] >= final_selling_rate[hour] + for hour, _ in final_selling_rate.items() + ): raise ValueError("final_buying_rate should be higher than final_selling_rate.") if energy_rate_change_per_update is not None and energy_rate_change_per_update < 0: raise ValueError("energy_rate_change_per_update should be a non-negative value.") @@ -380,8 +467,10 @@ def event_bid_traded(self, *, market_id, bid_trade): if bid_trade.buyer.name == self.owner.name: self.state.register_energy_from_bid_trade( - bid_trade.traded_energy, bid_trade.time_slot, - self._track_bought_energy_origin(bid_trade.seller.name)) + bid_trade.traded_energy, + bid_trade.time_slot, + self._track_bought_energy_origin(bid_trade.seller.name), + ) def _cycle_state(self): current_market = self.area.spot_market @@ -390,7 +479,7 @@ def _cycle_state(self): self.state.market_cycle( past_market.time_slot if past_market else None, current_market.time_slot, - self.area.future_market_time_slots + self.area.future_market_time_slots, ) def event_market_cycle(self): @@ -412,23 +501,24 @@ def event_balancing_market_cycle(self): current_market = self.area.spot_market free_storage = self.state.free_storage(current_market.time_slot) seller_details = TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid) + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ) if free_storage > 0: charge_energy = self.balancing_energy_ratio.demand * free_storage charge_price = DeviceRegistry.REGISTRY[self.owner.name][0] * charge_energy if charge_energy != 0 and charge_price != 0: # committing to start charging when required - self.area.get_balancing_market(self.area.now).balancing_offer(charge_price, - -charge_energy, - seller_details) + self.area.get_balancing_market(self.area.now).balancing_offer( + charge_price, -charge_energy, seller_details + ) if self.state.used_storage > 0: discharge_energy = self.balancing_energy_ratio.supply * self.state.used_storage discharge_price = DeviceRegistry.REGISTRY[self.owner.name][1] * discharge_energy # committing to start discharging when required if discharge_energy != 0 and discharge_price != 0: - self.area.get_balancing_market(self.area.now).balancing_offer(discharge_price, - discharge_energy, - seller_details) + self.area.get_balancing_market(self.area.now).balancing_offer( + discharge_price, discharge_energy, seller_details + ) def _try_to_buy_offer(self, offer, market, max_affordable_offer_rate): if offer.seller.name == self.owner.name: @@ -445,11 +535,18 @@ def _try_to_buy_offer(self, offer, market, max_affordable_offer_rate): max_energy = min(offer.energy, max_energy) if max_energy > FLOATING_POINT_TOLERANCE: self.state.register_energy_from_one_sided_market_accept_offer( - max_energy, market.time_slot, - self._track_bought_energy_origin(offer.seller.name)) - self.accept_offer(market, offer, buyer=TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid), - energy=max_energy) + max_energy, + market.time_slot, + self._track_bought_energy_origin(offer.seller.name), + ) + self.accept_offer( + market, + offer, + buyer=TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), + energy=max_energy, + ) return None except MarketException: # Offer already gone etc., try next one. @@ -466,8 +563,10 @@ def _buy_energy_one_sided_spot_market(self, market, offer=None): self._try_to_buy_offer(offer, market, max_affordable_offer_rate) else: for market_offer in market.sorted_offers: - if self._try_to_buy_offer( - market_offer, market, max_affordable_offer_rate) is False: + if ( + self._try_to_buy_offer(market_offer, market, max_affordable_offer_rate) + is False + ): return def _sell_energy_to_spot_market(self): @@ -475,9 +574,7 @@ def _sell_energy_to_spot_market(self): selling_rate = self.calculate_selling_rate(self.area.spot_market) energy_kWh = self.state.get_available_energy_to_sell_kWh(time_slot) if energy_kWh > FLOATING_POINT_TOLERANCE: - offer = self.post_first_offer( - self.area.spot_market, energy_kWh, selling_rate - ) + offer = self.post_first_offer(self.area.spot_market, energy_kWh, selling_rate) self.state.register_energy_from_posted_offer(offer.energy, time_slot) def _buy_energy_two_sided_spot_market(self): @@ -520,27 +617,35 @@ def _capacity_dependant_sell_rate(self, market): def _update_profiles_with_default_values(self): self.offer_update.update_and_populate_price_settings(self.area) self.bid_update.update_and_populate_price_settings(self.area) - self.state.add_default_values_to_state_profiles([ - self.spot_market_time_slot, *self.area.future_market_time_slots]) + self.state.add_default_values_to_state_profiles( + [self.spot_market_time_slot, *self.area.future_market_time_slots] + ) self._future_market_strategy.update_and_populate_price_settings(self) def event_offer(self, *, market_id, offer): super().event_offer(market_id=market_id, offer=offer) - if (ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.ONE_SIDED.value - and not self.area.is_market_future(market_id)): + if ( + ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.ONE_SIDED.value + and not self.area.is_market_future(market_id) + ): market = self.area.get_spot_or_future_market_by_id(market_id) if not market: return # sometimes the offer event arrives earlier than the market_cycle event, # so the default values have to be written here too: self._update_profiles_with_default_values() - if (offer.id in market.offers and offer.seller.name != self.owner.name and - offer.seller.name != self.area.name): + if ( + offer.id in market.offers + and offer.seller.name != self.owner.name + and offer.seller.name != self.area.name + ): self._buy_energy_one_sided_spot_market(market, offer) def _delete_past_state(self): - if (constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True or - self.area.current_market is None): + if ( + constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True + or self.area.current_market is None + ): return self.offer_update.delete_past_state_values(self.area.current_market.time_slot) @@ -548,8 +653,7 @@ def _delete_past_state(self): self.state.delete_past_state_values(self.area.current_market.time_slot) # Delete the state of the current slot from the future market cache - self._future_market_strategy.delete_past_state_values( - self.area.current_market.time_slot) + self._future_market_strategy.delete_past_state_values(self.area.current_market.time_slot) @property def asset_type(self):