| 
6 | 6 | import asyncio  | 
7 | 7 | import typing  | 
8 | 8 | import uuid  | 
 | 9 | +from collections import abc  | 
 | 10 | +from datetime import timedelta  | 
9 | 11 | 
 
  | 
10 | 12 | from ..._internal._channels import ReceiverFetcher  | 
11 | 13 | from ...actor import _power_managing  | 
 | 14 | +from ...timeseries import Bounds  | 
12 | 15 | from .._base_types import SystemBounds  | 
 | 16 | +from .._quantities import Power  | 
13 | 17 | from ._pv_pool_reference_store import PVPoolReferenceStore  | 
14 | 18 | from ._result_types import PVPoolReport  | 
15 | 19 | 
 
  | 
@@ -54,6 +58,79 @@ def __init__(  # pylint: disable=too-many-arguments  | 
54 | 58 |         self._source_id = str(unique_id) if name is None else f"{name}-{unique_id}"  | 
55 | 59 |         self._priority = priority  | 
56 | 60 | 
 
  | 
 | 61 | +    async def propose_power(  | 
 | 62 | +        self,  | 
 | 63 | +        power: Power | None,  | 
 | 64 | +        *,  | 
 | 65 | +        request_timeout: timedelta = timedelta(seconds=5.0),  | 
 | 66 | +        bounds: Bounds[Power | None] = Bounds(None, None),  | 
 | 67 | +    ) -> None:  | 
 | 68 | +        """Send a proposal to the power manager for the pool's set of PV inverters.  | 
 | 69 | +
  | 
 | 70 | +        This proposal is for the maximum power that can be set for the PV inverters in  | 
 | 71 | +        the pool.  The actual production might be lower.  | 
 | 72 | +
  | 
 | 73 | +        Power values need to follow the Passive Sign Convention (PSC). That is, positive  | 
 | 74 | +        values indicate charge power and negative values indicate discharge power.  | 
 | 75 | +        Only discharge powers are allowed for PV inverters.  | 
 | 76 | +
  | 
 | 77 | +        If the same PV inverters are shared by multiple actors, the power manager will  | 
 | 78 | +        consider the priority of the actors, the bounds they set, and their preferred  | 
 | 79 | +        power, when calculating the target power for the PV inverters.  | 
 | 80 | +
  | 
 | 81 | +        The preferred power of lower priority actors will take precedence as long as  | 
 | 82 | +        they respect the bounds set by higher priority actors.  If lower priority actors  | 
 | 83 | +        request power values outside of the bounds set by higher priority actors, the  | 
 | 84 | +        target power will be the closest value to the preferred power that is within the  | 
 | 85 | +        bounds.  | 
 | 86 | +
  | 
 | 87 | +        When there are no other actors trying to use the same PV inverters, the actor's  | 
 | 88 | +        preferred power would be set as the target power, as long as it falls within the  | 
 | 89 | +        system power bounds for the PV inverters.  | 
 | 90 | +
  | 
 | 91 | +        The result of the request can be accessed using the receiver returned from the  | 
 | 92 | +        [`power_status`][frequenz.sdk.timeseries.pv_pool.PVPool.power_status]  | 
 | 93 | +        method, which also streams the bounds that an actor should comply with, based on  | 
 | 94 | +        its priority.  | 
 | 95 | +
  | 
 | 96 | +        Args:  | 
 | 97 | +            power: The power to propose for the PV inverters in the pool.  If `None`,  | 
 | 98 | +                this proposal will not have any effect on the target power, unless  | 
 | 99 | +                bounds are specified.  If both are `None`, it is equivalent to not  | 
 | 100 | +                having a proposal or withdrawing a previous one.  | 
 | 101 | +            request_timeout: The timeout for the request.  | 
 | 102 | +            bounds: The power bounds for the proposal.  These bounds will apply to  | 
 | 103 | +                actors with a lower priority, and can be overridden by bounds from  | 
 | 104 | +                actors with a higher priority.  If None, the power bounds will be set to  | 
 | 105 | +                the maximum power of the batteries in the pool.  This is currently and  | 
 | 106 | +                experimental feature.  | 
 | 107 | +
  | 
 | 108 | +        Raises:  | 
 | 109 | +            PVPoolError: If a charge power for PV inverters is requested.  | 
 | 110 | +        """  | 
 | 111 | +        if power is not None and power > Power.zero():  | 
 | 112 | +            raise PVPoolError("Charge powers for PV inverters is not supported.")  | 
 | 113 | +        await self._pv_pool_ref.power_manager_requests_sender.send(  | 
 | 114 | +            _power_managing.Proposal(  | 
 | 115 | +                source_id=self._source_id,  | 
 | 116 | +                preferred_power=power,  | 
 | 117 | +                bounds=bounds,  | 
 | 118 | +                component_ids=self._pv_pool_ref.component_ids,  | 
 | 119 | +                priority=self._priority,  | 
 | 120 | +                creation_time=asyncio.get_running_loop().time(),  | 
 | 121 | +                request_timeout=request_timeout,  | 
 | 122 | +            )  | 
 | 123 | +        )  | 
 | 124 | + | 
 | 125 | +    @property  | 
 | 126 | +    def component_ids(self) -> abc.Set[int]:  | 
 | 127 | +        """Return component IDs of all PV inverters managed by this PVPool.  | 
 | 128 | +
  | 
 | 129 | +        Returns:  | 
 | 130 | +            Set of managed component IDs.  | 
 | 131 | +        """  | 
 | 132 | +        return self._pv_pool_ref.component_ids  | 
 | 133 | + | 
57 | 134 |     @property  | 
58 | 135 |     def power_status(self) -> ReceiverFetcher[PVPoolReport]:  | 
59 | 136 |         """Get a receiver to receive new power status reports when they change.  | 
 | 
0 commit comments