Exemple #1
0
 def setUp(self):
     self.ext_strategy_mock = MagicMock
     d3a.models.area.redis_external_connection.ExternalStrategy = self.ext_strategy_mock
     d3a.models.area.redis_external_connection.StrictRedis = MagicMock()
     redis_db_object = MagicMock()
     redis_db_object.pubsub = MagicMock
     d3a.models.area.redis_external_connection.StrictRedis.from_url = \
         MagicMock(return_value=redis_db_object)
     self.area = Area(name="base_area")
     self.external_connection = RedisAreaExternalConnection(self.area)
Exemple #2
0
    def __init__(self,
                 name: str = None,
                 children: List["Area"] = None,
                 uuid: str = None,
                 strategy: BaseStrategy = None,
                 appliance: BaseAppliance = None,
                 config: SimulationConfig = None,
                 budget_keeper=None,
                 balancing_spot_trade_ratio=ConstSettings.BalancingSettings.
                 SPOT_TRADE_RATIO,
                 event_list=[],
                 grid_fee_percentage: float = None,
                 transfer_fee_const: float = None,
                 external_connection_available=False):
        validate_area(grid_fee_percentage=grid_fee_percentage)
        self.balancing_spot_trade_ratio = balancing_spot_trade_ratio
        self.active = False
        self.log = TaggedLogWrapper(log, name)
        self.current_tick = 0
        self.name = name
        self.uuid = uuid if uuid is not None else str(uuid4())
        self.slug = slugify(name, to_lower=True)
        self.parent = None
        self.children = children if children is not None else []
        for child in self.children:
            child.parent = self

        if (len(self.children) > 0) and (strategy is not None):
            raise AreaException("A leaf area can not have children.")
        self.strategy = strategy
        self.appliance = appliance
        self._config = config
        self.events = Events(event_list, self)
        self.budget_keeper = budget_keeper
        if budget_keeper:
            self.budget_keeper.area = self
        self._bc = None
        self._markets = None
        self.dispatcher = DispatcherFactory(self)()
        self.grid_fee_percentage = grid_fee_percentage
        self.transfer_fee_const = transfer_fee_const
        self.display_type = "Area" if self.strategy is None else self.strategy.__class__.__name__
        self._markets = AreaMarkets(self.log)
        self.stats = AreaStats(self._markets)
        self.redis_ext_conn = RedisAreaExternalConnection(self) \
            if external_connection_available is True else None
Exemple #3
0
class Area:
    def __init__(self,
                 name: str = None,
                 children: List["Area"] = None,
                 uuid: str = None,
                 strategy: BaseStrategy = None,
                 appliance: BaseAppliance = None,
                 config: SimulationConfig = None,
                 budget_keeper=None,
                 balancing_spot_trade_ratio=ConstSettings.BalancingSettings.
                 SPOT_TRADE_RATIO,
                 event_list=[],
                 transfer_fee_pct: float = None,
                 transfer_fee_const: float = None,
                 external_connection_available=False):
        self.balancing_spot_trade_ratio = balancing_spot_trade_ratio
        self.active = False
        self.log = TaggedLogWrapper(log, name)
        self.current_tick = 0
        self.name = name
        self.uuid = uuid if uuid is not None else str(uuid4())
        self.slug = slugify(name, to_lower=True)
        self.parent = None
        self.children = children if children is not None else []
        for child in self.children:
            child.parent = self

        if (len(self.children) > 0) and (strategy is not None):
            raise AreaException("A leaf area can not have children.")
        self.strategy = strategy
        self.appliance = appliance
        self._config = config
        self.events = Events(event_list, self)
        self.budget_keeper = budget_keeper
        if budget_keeper:
            self.budget_keeper.area = self
        self._bc = None
        self._markets = None
        self.dispatcher = DispatcherFactory(self)()
        self.transfer_fee_pct = transfer_fee_pct
        self.transfer_fee_const = transfer_fee_const
        self.display_type = "Area" if self.strategy is None else self.strategy.__class__.__name__
        self._markets = AreaMarkets(self.log)
        self.stats = AreaStats(self._markets)
        self.redis_ext_conn = RedisAreaExternalConnection(self) \
            if external_connection_available is True else None

    def set_events(self, event_list):
        self.events = Events(event_list, self)

    def activate(self, bc=None):
        if bc:
            self._bc = bc
        for attr, kind in [(self.strategy, 'Strategy'),
                           (self.appliance, 'Appliance')]:
            if attr:
                if self.parent:
                    attr.area = self.parent
                    attr.owner = self
                else:
                    raise AreaException("{kind} {attr.__class__.__name__} "
                                        "on area {s} without parent!".format(
                                            kind=kind, attr=attr, s=self))

            if self.budget_keeper:
                self.budget_keeper.activate()
        if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0:
            self.transfer_fee_pct = 0
        elif self.transfer_fee_pct is None:
            self.transfer_fee_pct = self.config.iaa_fee
        if self.transfer_fee_const is None:
            self.transfer_fee_const = self.config.iaa_fee_const

        # Cycle markets without triggering it's own event chain.
        self._cycle_markets(_trigger_event=False)

        if not self.strategy and self.parent is not None:
            self.log.debug("No strategy. Using inter area agent.")
        self.log.debug('Activating area')
        self.active = True
        self.dispatcher.broadcast_activate()

    def deactivate(self):
        self._cycle_markets(deactivate=True)

    def _cycle_markets(self,
                       _trigger_event=True,
                       _market_cycle=False,
                       deactivate=False):
        """
        Remove markets for old time slots, add markets for new slots.
        Trigger `MARKET_CYCLE` event to allow child markets to also cycle.

        It's important for this to happen from top to bottom of the `Area` tree
        in order for the `InterAreaAgent`s to be connected correctly

        `_trigger_event` is used internally to avoid multiple event chains during
        initial area activation.
        """
        self.events.update_events(self.now)

        if self.redis_ext_conn:
            self.redis_ext_conn.market_cycle_event()

        if not self.children:
            # Since children trade in markets we only need to populate them if there are any
            return

        if self.budget_keeper and _market_cycle:
            self.budget_keeper.process_market_cycle()

        self.log.debug("Cycling markets")
        self._markets.rotate_markets(self.now, self.stats, self.dispatcher)
        if deactivate:
            return

        # Clear `current_market` cache
        self.__dict__.pop('current_market', None)

        # Markets range from one slot to market_count into the future
        changed = self._markets.create_future_markets(self.now, True, self)

        if ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET and \
                len(DeviceRegistry.REGISTRY.keys()) != 0:
            changed_balancing_market = self._markets.create_future_markets(
                self.now, False, self)
        else:
            changed_balancing_market = None

        # Force market cycle event in case this is the first market slot
        if (changed or len(self._markets.past_markets.keys())
                == 0) and _trigger_event:
            self.dispatcher.broadcast_market_cycle()

        # Force balancing_market cycle event in case this is the first market slot
        if (changed_balancing_market or len(self._markets.past_balancing_markets.keys()) == 0) \
                and _trigger_event and ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET:
            self.dispatcher.broadcast_balancing_market_cycle()

    def tick(self):
        if ConstSettings.IAASettings.MARKET_TYPE == 2 or \
                ConstSettings.IAASettings.MARKET_TYPE == 3:
            if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
                self.dispatcher.publish_market_clearing()
            else:
                for market in self.all_markets:
                    market.match_offers_bids()

        self.events.update_events(self.now)
        self.current_tick += 1
        if self._markets:
            for market in self._markets.markets.values():
                market.update_clock(self.current_tick)

    def tick_and_dispatch(self):
        if d3a.constants.DISPATCH_EVENTS_BOTTOM_TO_TOP:
            self.dispatcher.broadcast_tick()
            self.tick()
        else:
            self.tick()
            self.dispatcher.broadcast_tick()

    def __repr__(self):
        return "<Area '{s.name}' markets: {markets}>".format(
            s=self,
            markets=[
                t.format(TIME_FORMAT) for t in self._markets.markets.keys()
            ])

    @property
    def current_slot(self):
        return self.current_tick // self.config.ticks_per_slot

    @property
    def current_tick_in_slot(self):
        return self.current_tick % self.config.ticks_per_slot

    @property
    def config(self):
        if self._config:
            return self._config
        if self.parent:
            return self.parent.config
        return GlobalConfig

    @property
    def bc(self):
        if self._bc is not None:
            return self._bc
        if self.parent:
            return self.parent.bc
        return None

    @cached_property
    def child_by_slug(self):
        slug_map = {}
        areas = [self]
        while areas:
            for area in list(areas):
                slug_map[area.slug] = area
                areas.remove(area)
                areas.extend(area.children)
        return slug_map

    @property
    def now(self) -> DateTime:
        """
        Return the 'current time' as a `DateTime` object.
        Can be overridden in subclasses to change the meaning of 'now'.

        In this default implementation 'current time' is defined by the number of ticks that
        have passed.
        """
        return self.config.start_date.add(
            seconds=self.config.tick_length.seconds * self.current_tick)

    @property
    def all_markets(self):
        return [
            m for m in self._markets.markets.values()
            if is_market_in_simulation_duration(self.config, m)
        ]

    @property
    def past_markets(self):
        return list(self._markets.past_markets.values())

    def get_market(self, timeslot):
        return self._markets.markets[timeslot]

    def get_past_market(self, timeslot):
        return self._markets.past_markets[timeslot]

    def get_balancing_market(self, timeslot):
        return self._markets.balancing_markets[timeslot]

    @property
    def balancing_markets(self):
        return list(self._markets.balancing_markets.values())

    @property
    def past_balancing_markets(self):
        return list(self._markets.past_balancing_markets.values())

    @property
    def market_with_most_expensive_offer(self):
        # In case of a tie, max returns the first market occurrence in order to
        # satisfy the most recent market slot
        return max(
            self.all_markets,
            key=lambda m: m.sorted_offers[0].price / m.sorted_offers[0].energy)

    @property
    def next_market(self):
        """Returns the 'current' market (i.e. the one currently 'running')"""
        try:
            return list(self._markets.markets.values())[0]
        except IndexError:
            return None

    @property
    def current_market(self):
        """Returns the 'current' market (i.e. the one currently 'running')"""
        try:
            return list(self._markets.past_markets.values())[-1]
        except IndexError:
            return None

    @property
    def current_balancing_market(self):
        """Returns the 'current' balancing market (i.e. the one currently 'running')"""
        try:
            return list(self._markets.past_balancing_markets.values())[-1]
        except IndexError:
            return None

    def get_future_market_from_id(self, _id):
        try:
            return [m for m in self._markets.markets.values()
                    if m.id == _id][0]
        except IndexError:
            return None

    @property
    def last_past_market(self):
        try:
            return list(self._markets.past_markets.values())[-1]
        except IndexError:
            return None

    @cached_property
    def available_triggers(self):
        triggers = []
        if isinstance(self.strategy, TriggerMixin):
            triggers.extend(self.strategy.available_triggers)
        if isinstance(self.appliance, TriggerMixin):
            triggers.extend(self.appliance.available_triggers)
        return {t.name: t for t in triggers}

    def _fire_trigger(self, trigger_name, **params):
        for target in (self.strategy, self.appliance):
            if isinstance(target, TriggerMixin):
                for trigger in target.available_triggers:
                    if trigger.name == trigger_name:
                        return target.fire_trigger(trigger_name, **params)

    def update_config(self, **kwargs):
        if not self.config:
            return
        self.config.update_config_parameters(**kwargs)
        if self.strategy:
            self.strategy.read_config_event()
        for child in self.children:
            child.update_config(**kwargs)
class TestExternalConnectionRedis(unittest.TestCase):
    def setUp(self):
        self.ext_strategy_mock = MagicMock
        self.ext_strategy_mock.get_channel_list = lambda s: {}
        d3a.models.area.redis_external_connection.ExternalStrategy = self.ext_strategy_mock
        d3a.models.area.redis_external_connection.StrictRedis = MagicMock()
        redis_db_object = MagicMock()
        redis_db_object.pubsub = MagicMock
        d3a.models.area.redis_external_connection.StrictRedis.from_url = \
            MagicMock(return_value=redis_db_object)
        self.area = Area(name="base_area")
        self.external_connection = RedisAreaExternalConnection(self.area)

    def tearDown(self):
        pass

    def add_external_connections_to_area(self):
        area_list = ["kreuzberg", "friedrichschain", "prenzlauer berg"]
        self.external_connection.areas_to_register = area_list
        self.external_connection.register_new_areas()
        return area_list

    def test_external_connection_subscribes_to_register_unregister(self):
        self.external_connection.pubsub.subscribe.assert_called_once_with(
            **{
                "base-area/register_participant":
                self.external_connection.channel_register_callback,
                "base-area/unregister_participant":
                self.external_connection.channel_unregister_callback
            })

    def test_register_message_adds_area_to_buffer(self):
        self.external_connection.channel_register_callback(
            {"data": json.dumps({"name": "berlin"})})
        assert self.external_connection.areas_to_register == ["berlin"]

    def test_unregister_message_adds_area_to_buffer(self):
        self.external_connection.channel_unregister_callback(
            {"data": json.dumps({"name": "berlin"})})
        assert self.external_connection.areas_to_unregister == ["berlin"]

    def test_register_new_areas_creates_new_child_areas_from_buffer(self):
        area_list = self.add_external_connections_to_area()
        assert len(self.area.children) == 3
        assert not self.external_connection.areas_to_register
        assert set(ch.name for ch in self.area.children) == set(area_list)
        assert all(
            isinstance(ch.strategy, self.ext_strategy_mock)
            for ch in self.area.children)
        assert all(ch.parent == self.area for ch in self.area.children)
        assert all(ch.active for ch in self.area.children)
        assert self.external_connection.redis_db.publish.call_count == 3
        for i, created_area in enumerate(area_list):
            assert self.external_connection.redis_db.publish.call_args_list[i][0][0] == \
                "base-area/register_participant/response"

    def test_unregister_areas_deletes_children_from_area(self):
        area_list = self.add_external_connections_to_area()
        assert set(ch.name for ch in self.area.children) == set(area_list)
        # Keep track how many times the publish method was called when setting up the areas
        call_count = self.external_connection.redis_db.publish.call_count
        # Monkeypatch shutdown method of ExternalStrategy class
        for ch in self.area.children:
            ch.strategy.shutdown = MagicMock()

        self.external_connection.areas_to_unregister = ["prenzlauer berg"]
        self.external_connection.unregister_pending_areas()
        assert len(self.area.children) == 2
        assert self.external_connection.redis_db.publish.call_count == call_count + 1
        self.external_connection.redis_db.publish.assert_called_with(
            "base-area/unregister_participant/response",
            json.dumps({"response": "success"}))

        self.external_connection.areas_to_unregister = ["friedrichschain"]
        self.external_connection.unregister_pending_areas()
        assert len(self.area.children) == 1
        assert self.external_connection.redis_db.publish.call_count == call_count + 2
        self.external_connection.redis_db.publish.assert_called_with(
            "base-area/unregister_participant/response",
            json.dumps({"response": "success"}))

    def test_unregister_area_that_does_not_exist_returns_an_error(self):
        area_list = self.add_external_connections_to_area()
        assert set(ch.name for ch in self.area.children) == set(area_list)

        self.external_connection.areas_to_unregister = ["schoneberg"]
        self.external_connection.unregister_pending_areas()
        assert len(self.area.children) == 3
        self.external_connection.redis_db.publish.assert_called_with(
            "base-area/unregister_participant/response",
            json.dumps({"response": "failed"}))