Exemple #1
0
    def __init__(
            self,
            panel_count: int = 1,
            initial_selling_rate: float = ConstSettings.GeneralSettings.
        DEFAULT_MARKET_MAKER_RATE,
            final_selling_rate: float = ConstSettings.PVSettings.
        FINAL_SELLING_RATE,
            fit_to_limit: bool = True,
            update_interval=duration(
                minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL),
            energy_rate_decrease_per_update: float = ConstSettings.
        GeneralSettings.ENERGY_RATE_DECREASE_PER_UPDATE,
            max_panel_power_W: float = None,
            use_market_maker_rate: bool = False):
        """
        :param panel_count: Number of solar panels for this PV plant
        :param initial_selling_rate: Upper Threshold for PV offers
        :param final_selling_rate: Lower Threshold for PV offers
        :param fit_to_limit: Linear curve following initial_selling_rate & initial_selling_rate
        :param update_interval: Interval after which PV will update its offer
        :param energy_rate_decrease_per_update: Slope of PV Offer change per update
        :param max_panel_power_W:
        """

        # If use_market_maker_rate is true, overwrite initial_selling_rate to market maker rate
        if use_market_maker_rate:
            initial_selling_rate = GlobalConfig.market_maker_rate

        try:
            validate_pv_device(panel_count=panel_count,
                               max_panel_power_W=max_panel_power_W)
        except D3ADeviceException as e:
            raise D3ADeviceException(str(e))

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        BaseStrategy.__init__(self)
        self.offer_update = UpdateFrequencyMixin(
            initial_selling_rate, final_selling_rate, fit_to_limit,
            energy_rate_decrease_per_update, update_interval)
        for time_slot in generate_market_slot_list():
            try:
                validate_pv_device(
                    initial_selling_rate=self.offer_update.
                    initial_rate[time_slot],
                    final_selling_rate=self.offer_update.final_rate[time_slot])
            except D3ADeviceException as e:
                raise D3ADeviceException(str(e))
        self.panel_count = panel_count
        self.final_selling_rate = final_selling_rate
        self.max_panel_power_W = max_panel_power_W
        self.energy_production_forecast_kWh = {}  # type: Dict[Time, float]
        self.state = PVState()
Exemple #2
0
    def _init_price_update(self, update_interval, initial_selling_rate, final_selling_rate,
                           use_market_maker_rate, fit_to_limit, energy_rate_decrease_per_update):
        if update_interval is None:
            update_interval = \
                duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL)

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        self.final_selling_rate = final_selling_rate
        self.use_market_maker_rate = use_market_maker_rate
        validate_pv_device_price(fit_to_limit=fit_to_limit,
                                 energy_rate_decrease_per_update=energy_rate_decrease_per_update)

        self.offer_update = UpdateFrequencyMixin(initial_selling_rate, final_selling_rate,
                                                 fit_to_limit, energy_rate_decrease_per_update,
                                                 update_interval)
Exemple #3
0
    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,
                 max_panel_power_W: float = None,
                 use_market_maker_rate: bool = False):
        """
        :param panel_count: Number of solar panels for this PV plant
        :param initial_selling_rate: Upper Threshold for PV offers
        :param final_selling_rate: Lower Threshold for PV offers
        :param fit_to_limit: Linear curve following initial_selling_rate & initial_selling_rate
        :param update_interval: Interval after which PV will update its offer
        :param energy_rate_decrease_per_update: Slope of PV Offer change per update
        :param max_panel_power_W:
        """
        if update_interval is None:
            update_interval = \
                duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL)

        self.use_market_maker_rate = use_market_maker_rate

        validate_pv_device(
            panel_count=panel_count,
            max_panel_power_W=max_panel_power_W,
            fit_to_limit=fit_to_limit,
            energy_rate_decrease_per_update=energy_rate_decrease_per_update)

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        BaseStrategy.__init__(self)
        self.offer_update = UpdateFrequencyMixin(
            initial_selling_rate, final_selling_rate, fit_to_limit,
            energy_rate_decrease_per_update, update_interval)

        self.panel_count = panel_count
        self.final_selling_rate = final_selling_rate
        self.max_panel_power_W = max_panel_power_W
        self.energy_production_forecast_kWh = {}  # type: Dict[Time, float]
        self.state = PVState()
Exemple #4
0
    def _init_price_update(self, fit_to_limit, energy_rate_increase_per_update, update_interval,
                           use_market_maker_rate, initial_buying_rate, final_buying_rate):
        validate_load_device_price(fit_to_limit=fit_to_limit,
                                   energy_rate_increase_per_update=energy_rate_increase_per_update)

        if update_interval is None:
            update_interval = \
                duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL)

        self.use_market_maker_rate = use_market_maker_rate

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        BidEnabledStrategy.__init__(self)
        self.bid_update = \
            UpdateFrequencyMixin(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)
        self.fit_to_limit = fit_to_limit
Exemple #5
0
    def __init__(
            self,
            avg_power_W,
            hrs_per_day=None,
            hrs_of_day=None,
            fit_to_limit=True,
            energy_rate_increase_per_update=1,
            update_interval=duration(
                minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL),
            initial_buying_rate: Union[
                float, dict,
                str] = ConstSettings.LoadSettings.INITIAL_BUYING_RATE,
            final_buying_rate: Union[
                float, dict,
                str] = ConstSettings.LoadSettings.FINAL_BUYING_RATE,
            balancing_energy_ratio: tuple = (
                ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO,
                ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO),
            use_market_maker_rate: bool = False):
        """
        Constructor of LoadHoursStrategy
        :param avg_power_W: Power rating of load device
        :param hrs_per_day: Daily energy usage
        :param hrs_of_day: hours of day energy is needed
        :param fit_to_limit: if set to True, it will make a linear curve
        following following initial_buying_rate & final_buying_rate
        :param energy_rate_increase_per_update: Slope of Load bids change per update
        :param update_interval: Interval after which Load will update its offer
        :param initial_buying_rate: Starting point of load's preferred buying rate
        :param final_buying_rate: Ending point of load's preferred buying rate
        :param use_market_maker_rate: If set to True, Load would track its final buying rate
        as per utility's trading rate
        """

        # If use_market_maker_rate is true, overwrite final_buying_rate to market maker rate
        if use_market_maker_rate:
            final_buying_rate = GlobalConfig.market_maker_rate

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        BidEnabledStrategy.__init__(self)
        self.bid_update = \
            UpdateFrequencyMixin(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)
        try:
            validate_load_device(avg_power_W=avg_power_W,
                                 hrs_per_day=hrs_per_day,
                                 hrs_of_day=hrs_of_day)
        except D3ADeviceException as e:
            raise D3ADeviceException(str(e))

        for time_slot in generate_market_slot_list():
            rate_change = self.bid_update.energy_rate_change_per_update[
                time_slot]
            try:
                validate_load_device(
                    initial_buying_rate=self.bid_update.
                    initial_rate[time_slot],
                    final_buying_rate=self.bid_update.final_rate[time_slot],
                    energy_rate_increase_per_update=rate_change)
            except D3ADeviceException as e:
                raise D3ADeviceException(str(e))
        self.state = LoadState()
        self.avg_power_W = avg_power_W

        # consolidated_cycle is KWh energy consumed for the entire year
        self.daily_energy_required = None
        # Energy consumed during the day ideally should not exceed daily_energy_required
        self.energy_per_slot_Wh = None
        self.energy_requirement_Wh = {}  # type: Dict[Time, float]
        self.hrs_per_day = {}  # type: Dict[int, int]

        self.assign_hours_of_per_day(hrs_of_day, hrs_per_day)
        self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio)
Exemple #6
0
class LoadHoursStrategy(BidEnabledStrategy):
    parameters = ('avg_power_W', 'hrs_per_day', 'hrs_of_day', 'fit_to_limit',
                  'energy_rate_increase_per_update', 'update_interval',
                  'initial_buying_rate', 'final_buying_rate',
                  'balancing_energy_ratio', 'use_market_maker_rate')

    def __init__(
            self,
            avg_power_W,
            hrs_per_day=None,
            hrs_of_day=None,
            fit_to_limit=True,
            energy_rate_increase_per_update=1,
            update_interval=duration(
                minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL),
            initial_buying_rate: Union[
                float, dict,
                str] = ConstSettings.LoadSettings.INITIAL_BUYING_RATE,
            final_buying_rate: Union[
                float, dict,
                str] = ConstSettings.LoadSettings.FINAL_BUYING_RATE,
            balancing_energy_ratio: tuple = (
                ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO,
                ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO),
            use_market_maker_rate: bool = False):
        """
        Constructor of LoadHoursStrategy
        :param avg_power_W: Power rating of load device
        :param hrs_per_day: Daily energy usage
        :param hrs_of_day: hours of day energy is needed
        :param fit_to_limit: if set to True, it will make a linear curve
        following following initial_buying_rate & final_buying_rate
        :param energy_rate_increase_per_update: Slope of Load bids change per update
        :param update_interval: Interval after which Load will update its offer
        :param initial_buying_rate: Starting point of load's preferred buying rate
        :param final_buying_rate: Ending point of load's preferred buying rate
        :param use_market_maker_rate: If set to True, Load would track its final buying rate
        as per utility's trading rate
        """

        # If use_market_maker_rate is true, overwrite final_buying_rate to market maker rate
        if use_market_maker_rate:
            final_buying_rate = GlobalConfig.market_maker_rate

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        BidEnabledStrategy.__init__(self)
        self.bid_update = \
            UpdateFrequencyMixin(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)
        try:
            validate_load_device(avg_power_W=avg_power_W,
                                 hrs_per_day=hrs_per_day,
                                 hrs_of_day=hrs_of_day)
        except D3ADeviceException as e:
            raise D3ADeviceException(str(e))

        for time_slot in generate_market_slot_list():
            rate_change = self.bid_update.energy_rate_change_per_update[
                time_slot]
            try:
                validate_load_device(
                    initial_buying_rate=self.bid_update.
                    initial_rate[time_slot],
                    final_buying_rate=self.bid_update.final_rate[time_slot],
                    energy_rate_increase_per_update=rate_change)
            except D3ADeviceException as e:
                raise D3ADeviceException(str(e))
        self.state = LoadState()
        self.avg_power_W = avg_power_W

        # consolidated_cycle is KWh energy consumed for the entire year
        self.daily_energy_required = None
        # Energy consumed during the day ideally should not exceed daily_energy_required
        self.energy_per_slot_Wh = None
        self.energy_requirement_Wh = {}  # type: Dict[Time, float]
        self.hrs_per_day = {}  # type: Dict[int, int]

        self.assign_hours_of_per_day(hrs_of_day, hrs_per_day)
        self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio)

    @property
    def active_markets(self):
        return [
            market for market in self.area.all_markets
            if self._is_market_active(market)
        ]

    def _is_market_active(self, market):
        return self._allowed_operating_hours(market.time_slot) and \
            is_market_in_simulation_duration(self.area.config, market) and \
            (not self.area.current_market or
             market.time_slot >= self.area.current_market.time_slot)

    def assign_hours_of_per_day(self, hrs_of_day, hrs_per_day):
        if hrs_of_day is None:
            hrs_of_day = list(range(24))

        # be a parameter on the constructor or if we want to deal in percentages
        if hrs_per_day is None:
            hrs_per_day = len(hrs_of_day)
        if hrs_of_day is None:
            hrs_of_day = list(range(24))
        self.hrs_of_day = hrs_of_day
        self._initial_hrs_per_day = hrs_per_day

        if not all([0 <= h <= 23 for h in hrs_of_day]):
            raise ValueError(
                "Hrs_of_day list should contain integers between 0 and 23.")

        if len(hrs_of_day) < hrs_per_day:
            raise ValueError(
                "Length of list 'hrs_of_day' must be greater equal 'hrs_per_day'"
            )

    def assign_energy_requirement(self, avg_power_W):
        self.energy_per_slot_Wh = (
            avg_power_W / (duration(hours=1) / self.area.config.slot_length))
        for slot_time in generate_market_slot_list(area=self.area):
            if self._allowed_operating_hours(
                    slot_time) and slot_time >= self.area.now:
                self.energy_requirement_Wh[slot_time] = self.energy_per_slot_Wh
                self.state.desired_energy_Wh[
                    slot_time] = self.energy_per_slot_Wh

    def event_activate(self):
        self.bid_update.update_on_activate()
        self.hrs_per_day = {
            day: self._initial_hrs_per_day
            for day in range(self.area.config.sim_duration.days + 1)
        }
        self._simulation_start_timestamp = self.area.now
        self.assign_energy_requirement(self.avg_power_W)
        self._set_alternative_pricing_scheme()

    def area_reconfigure_event(self,
                               avg_power_W=None,
                               hrs_per_day=None,
                               hrs_of_day=None,
                               final_buying_rate=None):
        if hrs_per_day is not None or hrs_of_day is not None:
            self.assign_hours_of_per_day(hrs_of_day, hrs_per_day)
            self.hrs_per_day = {
                day: self._initial_hrs_per_day
                for day in range(self.area.config.sim_duration.days + 1)
            }

        if avg_power_W is not None:
            self.avg_power_W = avg_power_W
            self.assign_energy_requirement(avg_power_W)

        if final_buying_rate is not None:
            self.bid_update.final_rate = read_arbitrary_profile(
                InputProfileTypes.IDENTITY, final_buying_rate)

    def _find_acceptable_offer(self, market):
        offers = market.most_affordable_offers
        return random.choice(offers)

    def _one_sided_market_event_tick(self, market):
        try:
            if len(market.sorted_offers) < 1:
                return
            acceptable_offer = self._find_acceptable_offer(market)
            current_day = self._get_day_of_timestamp(market.time_slot)
            if acceptable_offer and \
                    self.hrs_per_day[current_day] > FLOATING_POINT_TOLERANCE and \
                    round(acceptable_offer.price / acceptable_offer.energy, 8) <= \
                    self.bid_update.final_rate[market.time_slot]:
                max_energy = self.energy_requirement_Wh[
                    market.time_slot] / 1000.0
                if max_energy < FLOATING_POINT_TOLERANCE:
                    return
                if acceptable_offer.energy > max_energy:
                    self.accept_offer(market,
                                      acceptable_offer,
                                      energy=max_energy,
                                      buyer_origin=self.owner.name)
                    self.energy_requirement_Wh[market.time_slot] = 0
                    self.hrs_per_day[current_day] -= self._operating_hours(
                        max_energy)
                else:
                    self.accept_offer(market,
                                      acceptable_offer,
                                      buyer_origin=self.owner.name)
                    self.energy_requirement_Wh[market.time_slot] -= \
                        acceptable_offer.energy * 1000.0
                    self.hrs_per_day[current_day] -= self._operating_hours(
                        acceptable_offer.energy)

        except MarketException:
            self.log.exception("An Error occurred while buying an offer")

    def _get_day_of_timestamp(self, time_slot):
        return (time_slot - self._simulation_start_timestamp).days

    def _double_sided_market_event_tick(self, market):
        self.bid_update.update_posted_bids_over_ticks(market, self)

    def event_tick(self):
        for market in self.active_markets:
            if market.time_slot not in self.energy_requirement_Wh:
                continue
            if self.energy_requirement_Wh[market.time_slot] <= 0:
                continue

            if ConstSettings.IAASettings.MARKET_TYPE == 1:
                self._one_sided_market_event_tick(market)
            elif ConstSettings.IAASettings.MARKET_TYPE == 2 or \
                    ConstSettings.IAASettings.MARKET_TYPE == 3:
                self._double_sided_market_event_tick(market)

    def event_offer(self, *, market_id, offer):
        super().event_offer(market_id=market_id, offer=offer)
        market = self.area.get_future_market_from_id(market_id)
        if market.time_slot in self.energy_requirement_Wh and \
                self._is_market_active(market) and \
                self.energy_requirement_Wh[market.time_slot] > FLOATING_POINT_TOLERANCE:
            if ConstSettings.IAASettings.MARKET_TYPE == 1:
                self._one_sided_market_event_tick(market)

    def _allowed_operating_hours(self, time):
        return time.hour in self.hrs_of_day

    def _operating_hours(self, energy):
        return (((energy * 1000) / self.energy_per_slot_Wh) *
                (self.area.config.slot_length / duration(hours=1)))

    def _set_alternative_pricing_scheme(self):
        if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0:
            for time_slot in generate_market_slot_list():
                final_rate = self.area.config.market_maker_rate[time_slot]
                self.bid_update.reassign_mixin_arguments(time_slot,
                                                         initial_rate=0,
                                                         final_rate=final_rate)

    def event_market_cycle(self):
        super().event_market_cycle()
        for market in self.active_markets:
            current_day = self._get_day_of_timestamp(market.time_slot)
            if self.hrs_per_day[current_day] <= FLOATING_POINT_TOLERANCE:
                self.energy_requirement_Wh[market.time_slot] = 0.0
                self.state.desired_energy_Wh[market.time_slot] = 0.0

            if ConstSettings.IAASettings.MARKET_TYPE == 2 or \
                    ConstSettings.IAASettings.MARKET_TYPE == 3:
                if self.energy_requirement_Wh[market.time_slot] > 0:
                    if self.is_eligible_for_balancing_market:
                        bid_energy = \
                            self.energy_requirement_Wh[market.time_slot] - \
                            self.balancing_energy_ratio.demand * \
                            self.state.desired_energy_Wh[market.time_slot]
                    else:
                        bid_energy = self.energy_requirement_Wh[
                            market.time_slot]
                    if not self.are_bids_posted(market.id):
                        self.post_first_bid(market, bid_energy)
                    else:
                        self.bid_update.update_market_cycle_bids(self)

    def event_balancing_market_cycle(self):
        for market in self.active_markets:
            self._demand_balancing_offer(market)

    def event_bid_traded(self, *, market_id, bid_trade):
        super().event_bid_traded(market_id=market_id, bid_trade=bid_trade)
        market = self.area.get_future_market_from_id(market_id)

        if bid_trade.offer.buyer == self.owner.name:
            self.energy_requirement_Wh[
                market.time_slot] -= bid_trade.offer.energy * 1000.0
            self.hrs_per_day[self._get_day_of_timestamp(market.time_slot)] -= \
                self._operating_hours(bid_trade.offer.energy)
            assert self.energy_requirement_Wh[market.time_slot] >= -FLOATING_POINT_TOLERANCE, \
                f"Energy requirement for load {self.owner.name} fell below zero " \
                f"({self.energy_requirement_Wh[market.time_slot]})."

    def event_trade(self, *, market_id, trade):
        market = self.area.get_future_market_from_id(market_id)
        assert market is not None

        if ConstSettings.BalancingSettings.FLEXIBLE_LOADS_SUPPORT:
            # Load can only put supply_balancing_offers only when there is a trade in spot_market
            self._supply_balancing_offer(market, trade)
        super().event_trade(market_id=market_id, trade=trade)

    # committing to increase its consumption when required
    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.desired_energy_Wh[market.time_slot]
        self.energy_requirement_Wh[market.time_slot] -= ramp_up_energy
        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,
                                self.owner.name)

    # committing to reduce its consumption when required
    def _supply_balancing_offer(self, market, trade):
        if not self.is_eligible_for_balancing_market:
            return
        if trade.buyer != self.owner.name:
            return
        ramp_down_energy = self.balancing_energy_ratio.supply * trade.offer.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, self.owner.name)
Exemple #7
0
Fichier : pv.py Projet : xg86/d3a
class PVStrategy(BaseStrategy):

    parameters = ('panel_count', 'initial_selling_rate', 'final_selling_rate',
                  'fit_to_limit', 'update_interval',
                  'energy_rate_decrease_per_update', 'max_panel_power_W',
                  'use_market_maker_rate')

    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,
                 max_panel_power_W: float = None,
                 use_market_maker_rate: bool = False):
        """
        :param panel_count: Number of solar panels for this PV plant
        :param initial_selling_rate: Upper Threshold for PV offers
        :param final_selling_rate: Lower Threshold for PV offers
        :param fit_to_limit: Linear curve following initial_selling_rate & initial_selling_rate
        :param update_interval: Interval after which PV will update its offer
        :param energy_rate_decrease_per_update: Slope of PV Offer change per update
        :param max_panel_power_W:
        """
        super().__init__()
        validate_pv_device_energy(panel_count=panel_count,
                                  max_panel_power_W=max_panel_power_W)

        self.panel_count = panel_count
        self.max_panel_power_W = max_panel_power_W
        self.energy_production_forecast_kWh = {}  # type: Dict[Time, float]
        self.state = PVState()

        self._init_price_update(update_interval, initial_selling_rate,
                                final_selling_rate, use_market_maker_rate,
                                fit_to_limit, energy_rate_decrease_per_update)

    def _init_price_update(self, update_interval, initial_selling_rate,
                           final_selling_rate, use_market_maker_rate,
                           fit_to_limit, energy_rate_decrease_per_update):
        if update_interval is None:
            update_interval = \
                duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL)

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        self.final_selling_rate = final_selling_rate
        self.use_market_maker_rate = use_market_maker_rate
        validate_pv_device_price(
            fit_to_limit=fit_to_limit,
            energy_rate_decrease_per_update=energy_rate_decrease_per_update)

        self.offer_update = UpdateFrequencyMixin(
            initial_selling_rate, final_selling_rate, fit_to_limit,
            energy_rate_decrease_per_update, update_interval)

    def area_reconfigure_event(self, validate=True, **kwargs):
        assert all(k in self.parameters for k in kwargs.keys())
        self._area_reconfigure_prices(validate, **kwargs)

        validate_pv_device_energy(**kwargs)
        self.produced_energy_forecast_kWh()
        for name, value in kwargs.items():
            setattr(self, name, value)

    def _area_reconfigure_prices(self, validate=True, **kwargs):
        if validate:
            validate_pv_device_price(**kwargs)

        if 'initial_selling_rate' in kwargs and kwargs[
                'initial_selling_rate'] is not None:
            self.offer_update.initial_rate = read_arbitrary_profile(
                InputProfileTypes.IDENTITY, kwargs['initial_selling_rate'])
        if 'final_selling_rate' in kwargs and kwargs[
                'final_selling_rate'] is not None:
            self.offer_update.final_rate = read_arbitrary_profile(
                InputProfileTypes.IDENTITY, kwargs['final_selling_rate'])

        self._validate_rates()
        self.offer_update.update_offer(self)

    def _validate_rates(self):
        for time_slot in generate_market_slot_list():
            validate_pv_device_price(
                initial_selling_rate=self.offer_update.initial_rate[time_slot],
                final_selling_rate=self.offer_update.final_rate[time_slot])

    def event_activate(self):
        self.event_activate_price()
        self.event_activate_energy()

    def event_activate_price(self):
        # If use_market_maker_rate is true, overwrite initial_selling_rate to market maker rate
        if self.use_market_maker_rate:
            self.area_reconfigure_event(
                initial_selling_rate=GlobalConfig.market_maker_rate,
                validate=False)
        self._validate_rates()
        # Calculating the produced energy
        self._set_alternative_pricing_scheme()
        self.offer_update.update_on_activate()

    def event_activate_energy(self):
        if self.max_panel_power_W is None:
            self.max_panel_power_W = self.area.config.max_panel_power_W
        self.produced_energy_forecast_kWh()

    def event_tick(self):
        self.offer_update.update_offer(self)
        self.offer_update.increment_update_counter_all_markets(self)

    def produced_energy_forecast_kWh(self):
        # This forecast ist based on the real PV system data provided by enphase
        # They can be found in the tools folder
        # A fit of a gaussian function to those data results in a formula Energy(time)
        for slot_time in generate_market_slot_list(area=self.area):
            if slot_time >= self.area.now:
                difference_to_midnight_in_minutes = \
                    slot_time.diff(self.area.now.start_of("day")).in_minutes() % (60 * 24)
                self.energy_production_forecast_kWh[slot_time] = \
                    self.gaussian_energy_forecast_kWh(
                        difference_to_midnight_in_minutes) * self.panel_count
                self.state.available_energy_kWh[slot_time] = \
                    self.energy_production_forecast_kWh[slot_time]
                assert self.energy_production_forecast_kWh[slot_time] >= 0.0

    def gaussian_energy_forecast_kWh(self, time_in_minutes=0):
        # The sun rises at approx 6:30 and sets at 18hr
        # time_in_minutes is the difference in time to midnight

        # Clamp to day range
        time_in_minutes %= 60 * 24

        if (8 * 60) > time_in_minutes or time_in_minutes > (16.5 * 60):
            gauss_forecast = 0

        else:
            gauss_forecast = self.max_panel_power_W * math.exp(
                # time/5 is needed because we only have one data set per 5 minutes
                (-(((round(time_in_minutes / 5, 0)) - 147.2) / 38.60)**2))
        # /1000 is needed to convert Wh into kWh
        w_to_wh_factor = (self.area.config.slot_length / duration(hours=1))
        return round((gauss_forecast / 1000) * w_to_wh_factor, 4)

    def event_market_cycle(self):
        super().event_market_cycle()
        self.event_market_cycle_price()

    def event_market_cycle_price(self):
        self.offer_update.update_market_cycle_offers(self)

        # Iterate over all markets open in the future
        for market in self.area.all_markets:
            assert self.state.available_energy_kWh[
                market.time_slot] >= -FLOATING_POINT_TOLERANCE
            if self.state.available_energy_kWh[market.time_slot] > 0:
                offer_price = \
                    self.offer_update.initial_rate[market.time_slot] * \
                    self.state.available_energy_kWh[market.time_slot]
                offer = market.offer(
                    offer_price,
                    self.state.available_energy_kWh[market.time_slot],
                    self.owner.name,
                    original_offer_price=offer_price,
                    seller_origin=self.owner.name)
                self.offers.post(offer, market.id)

    def event_trade(self, *, market_id, trade):
        super().event_trade(market_id=market_id, trade=trade)
        market = self.area.get_future_market_from_id(market_id)
        if market is None:
            return

        if trade.seller == self.owner.name:
            self.state.available_energy_kWh[
                market.time_slot] -= trade.offer.energy

    def _set_alternative_pricing_scheme(self):
        if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0:
            if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 1:
                for time_slot in generate_market_slot_list():
                    self.offer_update.reassign_mixin_arguments(time_slot,
                                                               initial_rate=0,
                                                               final_rate=0)
            elif ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 2:
                for time_slot in generate_market_slot_list():
                    rate = \
                        self.area.config.market_maker_rate[time_slot] * \
                        ConstSettings.IAASettings.AlternativePricing.FEED_IN_TARIFF_PERCENTAGE / \
                        100
                    self.offer_update.reassign_mixin_arguments(
                        time_slot, initial_rate=rate, final_rate=rate)
            elif ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 3:
                for time_slot in generate_market_slot_list():
                    rate = self.area.config.market_maker_rate[time_slot]
                    self.offer_update.reassign_mixin_arguments(
                        time_slot, initial_rate=rate, final_rate=rate)
            else:
                raise MarketException
Exemple #8
0
    def __init__(
        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,
        loss_per_hour=StorageSettings.LOSS_PER_HOUR,
        loss_function=StorageSettings.LOSS_FUNCTION,
        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)):

        if update_interval is None:
            update_interval = \
                duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL)

        if min_allowed_soc is None:
            min_allowed_soc = StorageSettings.MIN_ALLOWED_SOC
        self.initial_soc = initial_soc

        validate_storage_device(
            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,
            loss_per_hour=loss_per_hour,
            loss_function=loss_function,
            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)

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        BidEnabledStrategy.__init__(self)

        self.offer_update = \
            UpdateFrequencyMixin(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)
        for time_slot in generate_market_slot_list():
            validate_storage_device(
                initial_selling_rate=self.offer_update.initial_rate[time_slot],
                final_selling_rate=self.offer_update.final_rate[time_slot])
        self.bid_update = \
            UpdateFrequencyMixin(
                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
            )
        for time_slot in generate_market_slot_list():
            validate_storage_device(
                initial_buying_rate=self.bid_update.initial_rate[time_slot],
                final_buying_rate=self.bid_update.final_rate[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,
                         loss_per_hour=loss_per_hour,
                         loss_function=loss_function,
                         min_allowed_soc=min_allowed_soc)
        self.cap_price_strategy = cap_price_strategy
        self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio)
Exemple #9
0
class StorageStrategy(BidEnabledStrategy):
    parameters = ('initial_soc', 'min_allowed_soc', 'battery_capacity_kWh',
                  'max_abs_battery_power_kW', 'cap_price_strategy',
                  'initial_selling_rate', 'final_selling_rate',
                  'initial_buying_rate', 'final_buying_rate', 'fit_to_limit',
                  'energy_rate_increase_per_update',
                  'energy_rate_decrease_per_update', 'update_interval',
                  'initial_energy_origin', 'balancing_energy_ratio')

    def __init__(
        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,
        loss_per_hour=StorageSettings.LOSS_PER_HOUR,
        loss_function=StorageSettings.LOSS_FUNCTION,
        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)):

        if update_interval is None:
            update_interval = \
                duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL)

        if min_allowed_soc is None:
            min_allowed_soc = StorageSettings.MIN_ALLOWED_SOC
        self.initial_soc = initial_soc

        validate_storage_device(
            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,
            loss_per_hour=loss_per_hour,
            loss_function=loss_function,
            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)

        if isinstance(update_interval, int):
            update_interval = duration(minutes=update_interval)

        BidEnabledStrategy.__init__(self)

        self.offer_update = \
            UpdateFrequencyMixin(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)
        for time_slot in generate_market_slot_list():
            validate_storage_device(
                initial_selling_rate=self.offer_update.initial_rate[time_slot],
                final_selling_rate=self.offer_update.final_rate[time_slot])
        self.bid_update = \
            UpdateFrequencyMixin(
                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
            )
        for time_slot in generate_market_slot_list():
            validate_storage_device(
                initial_buying_rate=self.bid_update.initial_rate[time_slot],
                final_buying_rate=self.bid_update.final_rate[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,
                         loss_per_hour=loss_per_hour,
                         loss_function=loss_function,
                         min_allowed_soc=min_allowed_soc)
        self.cap_price_strategy = cap_price_strategy
        self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio)

    def _area_reconfigure_prices(self, **kwargs):
        if key_in_dict_and_not_none(kwargs, 'initial_selling_rate'):
            self.offer_update.initial_rate = read_arbitrary_profile(
                InputProfileTypes.IDENTITY, kwargs['initial_selling_rate'])
        if key_in_dict_and_not_none(kwargs, 'final_selling_rate'):
            self.offer_update.final_rate = read_arbitrary_profile(
                InputProfileTypes.IDENTITY, kwargs['final_selling_rate'])
        if key_in_dict_and_not_none(kwargs, 'initial_buying_rate'):
            self.bid_update.initial_rate = read_arbitrary_profile(
                InputProfileTypes.IDENTITY, kwargs['initial_buying_rate'])
        if key_in_dict_and_not_none(kwargs, 'final_buying_rate'):
            self.bid_update.final_rate = read_arbitrary_profile(
                InputProfileTypes.IDENTITY, kwargs['final_buying_rate'])
        if key_in_dict_and_not_none(kwargs, 'energy_rate_decrease_per_update'):
            self.offer_update.energy_rate_change_per_update = \
                read_arbitrary_profile(InputProfileTypes.IDENTITY,
                                       kwargs['energy_rate_decrease_per_update'])
        if key_in_dict_and_not_none(kwargs, 'energy_rate_increase_per_update'):
            self.bid_update.energy_rate_change_per_update = \
                read_arbitrary_profile(InputProfileTypes.IDENTITY,
                                       kwargs['energy_rate_increase_per_update'])
        if key_in_dict_and_not_none(kwargs, 'fit_to_limit'):
            self.bid_update.fit_to_limit = kwargs['fit_to_limit']
            self.offer_update.fit_to_limit = kwargs['fit_to_limit']
        if key_in_dict_and_not_none(kwargs, 'update_interval'):
            if isinstance(kwargs['update_interval'], int):
                update_interval = duration(minutes=kwargs['update_interval'])
            else:
                update_interval = kwargs['update_interval']
            self.bid_update.update_interval = update_interval
            self.offer_update.update_interval = update_interval

    def area_reconfigure_event(self, **kwargs):
        self._area_reconfigure_prices(**kwargs)
        self.offer_update.update_on_activate()
        self.bid_update.update_on_activate()
        self._validate_rates()

    def _validate_rates(self):
        for time_slot in generate_market_slot_list():
            bid_rate_change = None if self.bid_update.fit_to_limit else \
                self.bid_update.energy_rate_change_per_update[time_slot]
            offer_rate_change = None if self.offer_update.fit_to_limit else \
                self.offer_update.energy_rate_change_per_update[time_slot]
            validate_storage_device(
                initial_selling_rate=self.offer_update.initial_rate[time_slot],
                final_selling_rate=self.offer_update.final_rate[time_slot],
                initial_buying_rate=self.bid_update.initial_rate[time_slot],
                final_buying_rate=self.bid_update.final_rate[time_slot],
                energy_rate_increase_per_update=bid_rate_change,
                energy_rate_decrease_per_update=offer_rate_change,
                fit_to_limit=self.bid_update.fit_to_limit,
                update_interval=self.bid_update.update_interval)

    def event_on_disabled_area(self):
        self.state.calculate_soc_for_time_slot(self.area.next_market.time_slot)

    def event_activate_price(self):
        self.offer_update.update_on_activate()
        self.bid_update.update_on_activate()
        self._set_alternative_pricing_scheme()

    def event_activate_energy(self):
        self.state.set_battery_energy_per_slot(self.area.config.slot_length)

    def event_activate(self):
        self.event_activate_energy()
        self.event_activate_price()

    def _set_alternative_pricing_scheme(self):
        if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0:
            if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 1:
                for time_slot in generate_market_slot_list():
                    self.bid_update.reassign_mixin_arguments(time_slot,
                                                             initial_rate=0,
                                                             final_rate=0)
                    self.offer_update.reassign_mixin_arguments(time_slot,
                                                               initial_rate=0,
                                                               final_rate=0)
            elif ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 2:
                for time_slot in generate_market_slot_list():
                    rate = \
                        self.area.config.market_maker_rate[time_slot] * \
                        ConstSettings.IAASettings.AlternativePricing.FEED_IN_TARIFF_PERCENTAGE / \
                        100
                    self.bid_update.reassign_mixin_arguments(time_slot,
                                                             initial_rate=0,
                                                             final_rate=rate)
                    self.offer_update.reassign_mixin_arguments(
                        time_slot, initial_rate=rate, final_rate=rate)
            elif ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 3:
                for time_slot in generate_market_slot_list():
                    rate = self.area.config.market_maker_rate[time_slot]
                    self.bid_update.reassign_mixin_arguments(time_slot,
                                                             initial_rate=0,
                                                             final_rate=rate)
                    self.offer_update.reassign_mixin_arguments(
                        time_slot, initial_rate=rate, final_rate=rate)
            else:
                raise MarketException

    @staticmethod
    def _validate_constructor_arguments(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")
        if max_abs_battery_power_kW is not None and max_abs_battery_power_kW < 0:
            raise ValueError(
                "Battery Power rating must be a positive integer.")
        if initial_soc is not None and 0 < initial_soc > 100:
            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:
            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 type(final_selling_rate) is float and final_selling_rate < 0:
                raise ValueError("Final selling rate must be greater equal 0.")
            elif type(final_selling_rate) is 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()):
                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)
            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()):
                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()):
                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."
            )

    def event_tick(self):
        self.state.clamp_energy_to_buy_kWh(
            [ma.time_slot for ma in self.area.all_markets])

        for market in self.area.all_markets:
            if ConstSettings.IAASettings.MARKET_TYPE == 2 or \
                    ConstSettings.IAASettings.MARKET_TYPE == 3:
                self.state.clamp_energy_to_buy_kWh(
                    [ma.time_slot for ma in self.area.all_markets])
                if self.are_bids_posted(market.id):
                    self.bid_update.update_posted_bids_over_ticks(market, self)
                else:
                    energy_kWh = self.state.energy_to_buy_dict[
                        market.time_slot]
                    if energy_kWh > 0:
                        first_bid = self.post_first_bid(
                            market, energy_kWh * 1000.0)
                        if first_bid is not None:
                            self.state.offered_buy_kWh[
                                market.time_slot] += first_bid.energy

            self.state.tick(self.area, market.time_slot)
        if self.cap_price_strategy is False:
            self.offer_update.update_offer(self)

        self.bid_update.increment_update_counter_all_markets(self)
        if self.offer_update.increment_update_counter_all_markets(self):
            for market in self.area.all_markets:
                self.buy_energy(market)

    def event_trade(self, *, market_id, trade):
        market = self.area.get_future_market_from_id(market_id)
        super().event_trade(market_id=market_id, trade=trade)

        self.assert_if_trade_bid_price_is_too_high(market, trade)
        self.assert_if_trade_offer_price_is_too_low(market_id, trade)

        if trade.buyer == self.owner.name:
            self._track_energy_bought_type(trade)
        if trade.offer.seller == self.owner.name:
            self._track_energy_sell_type(trade)
            self.state.pledged_sell_kWh[market.time_slot] += trade.offer.energy
            self.state.offered_sell_kWh[market.time_slot] -= trade.offer.energy

    def _is_local(self, trade):
        for child in self.area.children:
            if child.name == trade.seller:
                return True

    # ESS Energy being utilized based on FIRST-IN FIRST-OUT mechanism
    def _track_energy_sell_type(self, trade):
        energy = trade.offer.energy
        while limit_float_precision(energy) > 0:
            first_in_energy_with_origin = self.state.get_used_storage_share[0]
            if energy >= first_in_energy_with_origin.value:
                energy -= first_in_energy_with_origin.value
                self.state.get_used_storage_share.pop(0)
            elif energy < first_in_energy_with_origin.value:
                residual = first_in_energy_with_origin.value - energy
                self.state._used_storage_share[0] = \
                    EnergyOrigin(first_in_energy_with_origin.origin, residual)
                energy = 0

    def _track_energy_bought_type(self, trade):
        if area_name_from_area_or_iaa_name(trade.seller) == self.area.name:
            self.state.update_used_storage_share(trade.offer.energy,
                                                 ESSEnergyOrigin.EXTERNAL)
        elif self._is_local(trade):
            self.state.update_used_storage_share(trade.offer.energy,
                                                 ESSEnergyOrigin.LOCAL)
        else:
            self.state.update_used_storage_share(trade.offer.energy,
                                                 ESSEnergyOrigin.UNKNOWN)

    def event_bid_traded(self, *, market_id, bid_trade):
        super().event_bid_traded(market_id=market_id, bid_trade=bid_trade)
        market = self.area.get_future_market_from_id(market_id)

        if bid_trade.offer.buyer == self.owner.name:
            self._track_energy_bought_type(bid_trade)
            self.state.pledged_buy_kWh[
                market.time_slot] += bid_trade.offer.energy
            self.state.offered_buy_kWh[
                market.time_slot] -= bid_trade.offer.energy

    def event_market_cycle(self):
        super().event_market_cycle()
        self.offer_update.update_market_cycle_offers(self)
        for market in self.area.all_markets[:-1]:
            self.bid_update.update_counter[market.time_slot] = 0
        current_market = self.area.next_market
        past_market = self.area.last_past_market

        self.state.market_cycle(
            past_market.time_slot if past_market else current_market.time_slot,
            current_market.time_slot)

        if self.state.used_storage > 0:
            self.sell_energy()

        if ConstSettings.IAASettings.MARKET_TYPE == 2 or \
           ConstSettings.IAASettings.MARKET_TYPE == 3:
            self.state.clamp_energy_to_buy_kWh([current_market.time_slot])
            self.bid_update.update_market_cycle_bids(self)
            energy_kWh = self.state.energy_to_buy_dict[
                current_market.time_slot]
            if energy_kWh > 0:
                self.post_first_bid(current_market, energy_kWh * 1000.0)
                self.state.offered_buy_kWh[
                    current_market.time_slot] += energy_kWh

    def event_balancing_market_cycle(self):
        if not self.is_eligible_for_balancing_market:
            return

        current_market = self.area.next_market
        free_storage = self.state.free_storage(current_market.time_slot)
        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, self.owner.name)
        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, self.owner.name)

    def _try_to_buy_offer(self, offer, market, max_affordable_offer_rate):
        if offer.seller == self.owner.name:
            # Don't buy our own offer
            return
        # Check if the price is cheap enough
        if offer.energy_rate > max_affordable_offer_rate:
            # Can early return here, because the offers are sorted according to energy rate
            # therefore the following offers will be more expensive
            return True
        alt_pricing_settings = ConstSettings.IAASettings.AlternativePricing
        if offer.seller == alt_pricing_settings.ALT_PRICING_MARKET_MAKER_NAME \
                and alt_pricing_settings.PRICING_SCHEME != 0:
            # don't buy from IAA if alternative pricing scheme is activated
            return

        try:
            self.state.clamp_energy_to_buy_kWh(
                [ma.time_slot for ma in self.area.all_markets])
            max_energy = min(offer.energy,
                             self.state.energy_to_buy_dict[market.time_slot])
            if not self.state.has_battery_reached_max_power(
                    -max_energy, market.time_slot):
                self.state.pledged_buy_kWh[market.time_slot] += max_energy
                self.accept_offer(market,
                                  offer,
                                  energy=max_energy,
                                  buyer_origin=self.owner.name)
            return
        except MarketException:
            # Offer already gone etc., try next one.
            return

    def buy_energy(self, market, offer=None):
        if not market:
            return
        if self.state.has_battery_reached_max_power(-FLOATING_POINT_TOLERANCE,
                                                    market.time_slot):
            return
        max_affordable_offer_rate = min(
            self.bid_update.get_updated_rate(market.time_slot),
            self.bid_update.final_rate[market.time_slot])
        # Check if storage has free capacity
        if self.state.free_storage(market.time_slot) <= 0.0:
            return

        if offer:
            self._try_to_buy_offer(offer, market, max_affordable_offer_rate)
        else:
            for offer in market.sorted_offers:
                if self._try_to_buy_offer(offer, market,
                                          max_affordable_offer_rate) is False:
                    return

    def sell_energy(self):
        markets_to_sell = self.select_market_to_sell()
        energy_sell_dict = self.state.clamp_energy_to_sell_kWh(
            [ma.time_slot for ma in markets_to_sell])
        for market in markets_to_sell:
            selling_rate = self.calculate_selling_rate(market)
            energy = energy_sell_dict[market.time_slot]
            if not self.state.has_battery_reached_max_power(
                    energy, market.time_slot):
                if energy > 0.0:
                    offer = market.offer(price=energy * selling_rate,
                                         energy=energy,
                                         seller=self.owner.name,
                                         original_offer_price=energy *
                                         selling_rate,
                                         seller_origin=self.owner.name)
                    self.offers.post(offer, market.id)
                    self.state.offered_sell_kWh[
                        market.time_slot] += offer.energy

    def select_market_to_sell(self):
        if StorageSettings.SELL_ON_MOST_EXPENSIVE_MARKET:
            # Sell on the most expensive market
            try:
                max_rate = 0.0
                most_expensive_market = self.area.all_markets[0]
                for market in self.area.all_markets:
                    if len(market.sorted_offers) > 0 and \
                       market.sorted_offers[0].energy_rate > max_rate:
                        max_rate = market.sorted_offers[
                            0].price / market.sorted_offers[0].energy
                        most_expensive_market = market
            except IndexError:
                try:
                    most_expensive_market = self.area.current_market
                except StopIteration:
                    return
            return [most_expensive_market]
        else:
            return self.area.all_markets

    def calculate_selling_rate(self, market):
        if self.cap_price_strategy is True:
            return self.capacity_dependant_sell_rate(market)
        else:
            return self.offer_update.initial_rate[market.time_slot]

    def capacity_dependant_sell_rate(self, market):
        if self.state.charge_history[market.time_slot] == "-":
            soc = self.state.used_storage / self.state.capacity
        else:
            soc = self.state.charge_history[market.time_slot] / 100.0
        max_selling_rate = self.offer_update.initial_rate[market.time_slot]
        min_selling_rate = self.offer_update.final_rate[market.time_slot]
        if max_selling_rate < min_selling_rate:
            return min_selling_rate
        else:
            return max_selling_rate - (max_selling_rate -
                                       min_selling_rate) * soc

    def event_offer(self, *, market_id, offer):
        super().event_offer(market_id=market_id, offer=offer)
        if ConstSettings.IAASettings.MARKET_TYPE == 1:
            market = self.area.get_future_market_from_id(market_id)
            if offer.id in market.offers and \
                    offer.seller != self.owner.name and \
                    offer.seller != self.area.name:
                self.buy_energy(market, offer)