|
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