|  | 
| 10 | 10 | 
 | 
| 11 | 11 | from __future__ import annotations | 
| 12 | 12 | 
 | 
|  | 13 | +import logging | 
| 13 | 14 | import sys | 
| 14 | 15 | import typing | 
| 15 | 16 | from collections import abc | 
|  | 
| 44 | 45 |     from ..timeseries.logical_meter import LogicalMeter | 
| 45 | 46 |     from ..timeseries.producer import Producer | 
| 46 | 47 | 
 | 
|  | 48 | +_logger = logging.getLogger(__name__) | 
|  | 49 | + | 
| 47 | 50 | 
 | 
| 48 | 51 | _REQUEST_RECV_BUFFER_SIZE = 500 | 
| 49 | 52 | """The maximum number of requests that can be queued in the request receiver. | 
| @@ -103,11 +106,22 @@ def __init__( | 
| 103 | 106 |         self._consumer: Consumer | None = None | 
| 104 | 107 |         self._producer: Producer | None = None | 
| 105 | 108 |         self._grid: Grid | None = None | 
| 106 |  | -        self._ev_charger_pools: dict[frozenset[int], EVChargerPoolReferenceStore] = {} | 
| 107 |  | -        self._battery_pools: dict[frozenset[int], BatteryPoolReferenceStore] = {} | 
|  | 109 | +        self._ev_charger_pool_reference_stores: dict[ | 
|  | 110 | +            frozenset[int], EVChargerPoolReferenceStore | 
|  | 111 | +        ] = {} | 
|  | 112 | +        self._battery_pool_reference_stores: dict[ | 
|  | 113 | +            frozenset[int], BatteryPoolReferenceStore | 
|  | 114 | +        ] = {} | 
| 108 | 115 |         self._frequency_instance: GridFrequency | None = None | 
| 109 | 116 |         self._voltage_instance: VoltageStreamer | None = None | 
| 110 | 117 | 
 | 
|  | 118 | +        self._known_pool_keys: set[str] = set() | 
|  | 119 | +        """A set of keys for corresponding to created EVChargerPool instances. | 
|  | 120 | +
 | 
|  | 121 | +        This is used to warn the user if they try to create a new EVChargerPool instance | 
|  | 122 | +        for the same set of component IDs, and with the same priority. | 
|  | 123 | +        """ | 
|  | 124 | + | 
| 111 | 125 |     def frequency(self) -> GridFrequency: | 
| 112 | 126 |         """Return the grid frequency measuring point.""" | 
| 113 | 127 |         if self._frequency_instance is None: | 
| @@ -191,26 +205,45 @@ def ev_charger_pool( | 
| 191 | 205 |             self._ev_power_wrapper.start() | 
| 192 | 206 | 
 | 
| 193 | 207 |         # We use frozenset to make a hashable key from the input set. | 
| 194 |  | -        key: frozenset[int] = frozenset() | 
|  | 208 | +        ref_store_key: frozenset[int] = frozenset() | 
| 195 | 209 |         if ev_charger_ids is not None: | 
| 196 |  | -            key = frozenset(ev_charger_ids) | 
| 197 |  | - | 
| 198 |  | -        if key not in self._ev_charger_pools: | 
| 199 |  | -            self._ev_charger_pools[key] = EVChargerPoolReferenceStore( | 
| 200 |  | -                channel_registry=self._channel_registry, | 
| 201 |  | -                resampler_subscription_sender=self._resampling_request_sender(), | 
| 202 |  | -                status_receiver=self._ev_power_wrapper.status_channel.new_receiver( | 
| 203 |  | -                    limit=1 | 
| 204 |  | -                ), | 
| 205 |  | -                power_manager_requests_sender=( | 
| 206 |  | -                    self._ev_power_wrapper.proposal_channel.new_sender() | 
| 207 |  | -                ), | 
| 208 |  | -                power_manager_bounds_subs_sender=( | 
| 209 |  | -                    self._ev_power_wrapper.bounds_subscription_channel.new_sender() | 
| 210 |  | -                ), | 
| 211 |  | -                component_ids=ev_charger_ids, | 
|  | 210 | +            ref_store_key = frozenset(ev_charger_ids) | 
|  | 211 | + | 
|  | 212 | +        pool_key = f"{ref_store_key}-{priority}" | 
|  | 213 | +        if pool_key in self._known_pool_keys: | 
|  | 214 | +            _logger.warning( | 
|  | 215 | +                "An EVChargerPool instance was already created for ev_charger_ids=%s " | 
|  | 216 | +                "and priority=%s using `microgrid.ev_charger_pool(...)`." | 
|  | 217 | +                "\n  Hint: If the multiple instances are created from the same actor, " | 
|  | 218 | +                "consider reusing the same instance." | 
|  | 219 | +                "\n  Hint: If the instances are created from different actors, " | 
|  | 220 | +                "consider using different priorities to distinguish them.", | 
|  | 221 | +                ev_charger_ids, | 
|  | 222 | +                priority, | 
|  | 223 | +            ) | 
|  | 224 | +        else: | 
|  | 225 | +            self._known_pool_keys.add(pool_key) | 
|  | 226 | + | 
|  | 227 | +        if ref_store_key not in self._ev_charger_pool_reference_stores: | 
|  | 228 | +            self._ev_charger_pool_reference_stores[ref_store_key] = ( | 
|  | 229 | +                EVChargerPoolReferenceStore( | 
|  | 230 | +                    channel_registry=self._channel_registry, | 
|  | 231 | +                    resampler_subscription_sender=self._resampling_request_sender(), | 
|  | 232 | +                    status_receiver=self._ev_power_wrapper.status_channel.new_receiver( | 
|  | 233 | +                        limit=1 | 
|  | 234 | +                    ), | 
|  | 235 | +                    power_manager_requests_sender=( | 
|  | 236 | +                        self._ev_power_wrapper.proposal_channel.new_sender() | 
|  | 237 | +                    ), | 
|  | 238 | +                    power_manager_bounds_subs_sender=( | 
|  | 239 | +                        self._ev_power_wrapper.bounds_subscription_channel.new_sender() | 
|  | 240 | +                    ), | 
|  | 241 | +                    component_ids=ev_charger_ids, | 
|  | 242 | +                ) | 
| 212 | 243 |             ) | 
| 213 |  | -        return EVChargerPool(self._ev_charger_pools[key], name, priority) | 
|  | 244 | +        return EVChargerPool( | 
|  | 245 | +            self._ev_charger_pool_reference_stores[ref_store_key], name, priority | 
|  | 246 | +        ) | 
| 214 | 247 | 
 | 
| 215 | 248 |     def grid(self) -> Grid: | 
| 216 | 249 |         """Return the grid measuring point.""" | 
| @@ -253,28 +286,47 @@ def battery_pool( | 
| 253 | 286 |             self._battery_power_wrapper.start() | 
| 254 | 287 | 
 | 
| 255 | 288 |         # We use frozenset to make a hashable key from the input set. | 
| 256 |  | -        key: frozenset[int] = frozenset() | 
|  | 289 | +        ref_store_key: frozenset[int] = frozenset() | 
| 257 | 290 |         if battery_ids is not None: | 
| 258 |  | -            key = frozenset(battery_ids) | 
| 259 |  | - | 
| 260 |  | -        if key not in self._battery_pools: | 
| 261 |  | -            self._battery_pools[key] = BatteryPoolReferenceStore( | 
| 262 |  | -                channel_registry=self._channel_registry, | 
| 263 |  | -                resampler_subscription_sender=self._resampling_request_sender(), | 
| 264 |  | -                batteries_status_receiver=self._battery_power_wrapper.status_channel.new_receiver( | 
| 265 |  | -                    limit=1 | 
| 266 |  | -                ), | 
| 267 |  | -                power_manager_requests_sender=( | 
| 268 |  | -                    self._battery_power_wrapper.proposal_channel.new_sender() | 
| 269 |  | -                ), | 
| 270 |  | -                power_manager_bounds_subscription_sender=( | 
| 271 |  | -                    self._battery_power_wrapper.bounds_subscription_channel.new_sender() | 
| 272 |  | -                ), | 
| 273 |  | -                min_update_interval=self._resampler_config.resampling_period, | 
| 274 |  | -                batteries_id=battery_ids, | 
|  | 291 | +            ref_store_key = frozenset(battery_ids) | 
|  | 292 | + | 
|  | 293 | +        pool_key = f"{ref_store_key}-{priority}" | 
|  | 294 | +        if pool_key in self._known_pool_keys: | 
|  | 295 | +            _logger.warning( | 
|  | 296 | +                "A BatteryPool instance was already created for battery_ids=%s and " | 
|  | 297 | +                "priority=%s using `microgrid.battery_pool(...)`." | 
|  | 298 | +                "\n  Hint: If the multiple instances are created from the same actor, " | 
|  | 299 | +                "consider reusing the same instance." | 
|  | 300 | +                "\n  Hint: If the instances are created from different actors, " | 
|  | 301 | +                "consider using different priorities to distinguish them.", | 
|  | 302 | +                battery_ids, | 
|  | 303 | +                priority, | 
|  | 304 | +            ) | 
|  | 305 | +        else: | 
|  | 306 | +            self._known_pool_keys.add(pool_key) | 
|  | 307 | + | 
|  | 308 | +        if ref_store_key not in self._battery_pool_reference_stores: | 
|  | 309 | +            self._battery_pool_reference_stores[ref_store_key] = ( | 
|  | 310 | +                BatteryPoolReferenceStore( | 
|  | 311 | +                    channel_registry=self._channel_registry, | 
|  | 312 | +                    resampler_subscription_sender=self._resampling_request_sender(), | 
|  | 313 | +                    batteries_status_receiver=( | 
|  | 314 | +                        self._battery_power_wrapper.status_channel.new_receiver(limit=1) | 
|  | 315 | +                    ), | 
|  | 316 | +                    power_manager_requests_sender=( | 
|  | 317 | +                        self._battery_power_wrapper.proposal_channel.new_sender() | 
|  | 318 | +                    ), | 
|  | 319 | +                    power_manager_bounds_subscription_sender=( | 
|  | 320 | +                        self._battery_power_wrapper.bounds_subscription_channel.new_sender() | 
|  | 321 | +                    ), | 
|  | 322 | +                    min_update_interval=self._resampler_config.resampling_period, | 
|  | 323 | +                    batteries_id=battery_ids, | 
|  | 324 | +                ) | 
| 275 | 325 |             ) | 
| 276 | 326 | 
 | 
| 277 |  | -        return BatteryPool(self._battery_pools[key], name, priority) | 
|  | 327 | +        return BatteryPool( | 
|  | 328 | +            self._battery_pool_reference_stores[ref_store_key], name, priority | 
|  | 329 | +        ) | 
| 278 | 330 | 
 | 
| 279 | 331 |     def _data_sourcing_request_sender(self) -> Sender[ComponentMetricRequest]: | 
| 280 | 332 |         """Return a Sender for sending requests to the data sourcing actor. | 
| @@ -331,7 +383,7 @@ async def _stop(self) -> None: | 
| 331 | 383 |         if self._resampling_actor: | 
| 332 | 384 |             await self._resampling_actor.actor.stop() | 
| 333 | 385 |         await self._battery_power_wrapper.stop() | 
| 334 |  | -        for pool in self._battery_pools.values(): | 
|  | 386 | +        for pool in self._battery_pool_reference_stores.values(): | 
| 335 | 387 |             await pool.stop() | 
| 336 | 388 | 
 | 
| 337 | 389 | 
 | 
| @@ -393,6 +445,15 @@ def ev_charger_pool( | 
| 393 | 445 |         When specifying priority, bigger values indicate higher priority. The default | 
| 394 | 446 |         priority is the lowest possible value. | 
| 395 | 447 | 
 | 
|  | 448 | +        It is recommended to reuse the same instance of the `EVChargerPool` within the | 
|  | 449 | +        same actor, unless they are managing different sets of EV chargers. | 
|  | 450 | +
 | 
|  | 451 | +        In deployments with multiple actors managing the same set of EV chargers, it is | 
|  | 452 | +        recommended to use different priorities to distinguish between them.  If not, | 
|  | 453 | +        a random prioritization will be imposed on them to resolve conflicts, which may | 
|  | 454 | +        lead to unexpected behavior like longer duration to converge on the desired | 
|  | 455 | +        power. | 
|  | 456 | +
 | 
| 396 | 457 |     Args: | 
| 397 | 458 |         ev_charger_ids: Optional set of IDs of EV Chargers to be managed by the | 
| 398 | 459 |             EVChargerPool.  If not specified, all EV Chargers available in the | 
| @@ -421,6 +482,15 @@ def battery_pool( | 
| 421 | 482 |         When specifying priority, bigger values indicate higher priority. The default | 
| 422 | 483 |         priority is the lowest possible value. | 
| 423 | 484 | 
 | 
|  | 485 | +        It is recommended to reuse the same instance of the `BatteryPool` within the | 
|  | 486 | +        same actor, unless they are managing different sets of batteries. | 
|  | 487 | +
 | 
|  | 488 | +        In deployments with multiple actors managing the same set of batteries, it is | 
|  | 489 | +        recommended to use different priorities to distinguish between them.  If not, | 
|  | 490 | +        a random prioritization will be imposed on them to resolve conflicts, which may | 
|  | 491 | +        lead to unexpected behavior like longer duration to converge on the desired | 
|  | 492 | +        power. | 
|  | 493 | +
 | 
| 424 | 494 |     Args: | 
| 425 | 495 |         battery_ids: Optional set of IDs of batteries to be managed by the `BatteryPool`. | 
| 426 | 496 |             If not specified, all batteries available in the component graph are used. | 
|  | 
0 commit comments