1818
1919from frequenz .channels import Broadcast , Sender
2020from frequenz .client .common .microgrid .components import ComponentId
21- from frequenz .client .microgrid .component import Battery , EvCharger , SolarInverter
21+ from frequenz .client .microgrid .component import (
22+ Battery ,
23+ EvCharger ,
24+ SolarInverter ,
25+ SteamBoiler ,
26+ )
2227
2328from .._internal ._channels import ChannelRegistry
2429from ..actor ._actor import Actor
2530from ..timeseries import ResamplerConfig
2631from ..timeseries ._voltage_streamer import VoltageStreamer
32+ from ..timeseries .steam_boiler_pool import SteamBoilerPool
33+ from ..timeseries .steam_boiler_pool ._steam_boiler_pool_reference_store import (
34+ SteamBoilerPoolReferenceStore ,
35+ )
2736from ._data_sourcing import ComponentMetricRequest , DataSourcingActor
2837from ._power_managing ._base_classes import DefaultPower , PowerManagerAlgorithm
2938from ._power_wrapper import PowerWrapper
@@ -127,6 +136,13 @@ def __init__(
127136 # https://github.com/frequenz-floss/frequenz-sdk-python/issues/1285
128137 component_class = SolarInverter ,
129138 )
139+ self ._steam_boiler_power_wrapper = PowerWrapper (
140+ self ._channel_registry ,
141+ api_power_request_timeout = api_power_request_timeout ,
142+ power_manager_algorithm = PowerManagerAlgorithm .MATRYOSHKA ,
143+ default_power = DefaultPower .MAX ,
144+ component_class = SteamBoiler ,
145+ )
130146
131147 self ._logical_meter : LogicalMeter | None = None
132148 self ._consumer : Consumer | None = None
@@ -141,6 +157,9 @@ def __init__(
141157 self ._pv_pool_reference_stores : dict [
142158 frozenset [ComponentId ], PVPoolReferenceStore
143159 ] = {}
160+ self ._steam_boiler_pool_reference_stores : dict [
161+ frozenset [ComponentId ], SteamBoilerPoolReferenceStore
162+ ] = {}
144163 self ._frequency_instance : GridFrequency | None = None
145164 self ._voltage_instance : VoltageStreamer | None = None
146165
@@ -447,6 +466,82 @@ def new_battery_pool(
447466 priority = priority ,
448467 )
449468
469+ def new_steam_boiler_pool (
470+ self ,
471+ * ,
472+ priority : int ,
473+ component_ids : abc .Set [ComponentId ] | None = None ,
474+ name : str | None = None ,
475+ ) -> SteamBoilerPool :
476+ """Return the corresponding SteamBoilerPool instance for the given ids.
477+
478+ If a SteamBoilerPool instance for the given ids doesn't exist, a new one is
479+ created and returned.
480+
481+ Args:
482+ priority: The priority of the actor making the call.
483+ component_ids: Optional set of IDs of steam boilers to be managed by the
484+ SteamBoilerPool.
485+ name: An optional name used to identify this instance of the pool or a
486+ corresponding actor in the logs.
487+
488+ Returns:
489+ A SteamBoilerPool instance.
490+ """
491+ from ..timeseries .steam_boiler_pool import SteamBoilerPool
492+ from ..timeseries .steam_boiler_pool ._steam_boiler_pool_reference_store import (
493+ SteamBoilerPoolReferenceStore ,
494+ )
495+
496+ if not self ._steam_boiler_power_wrapper .started :
497+ self ._steam_boiler_power_wrapper .start ()
498+
499+ # We use frozenset to make a hashable key from the input set.
500+ ref_store_key : frozenset [ComponentId ] = frozenset ()
501+ if component_ids is not None :
502+ ref_store_key = frozenset (component_ids )
503+
504+ pool_key = f"{ ref_store_key } -{ priority } "
505+ if pool_key in self ._known_pool_keys :
506+ _logger .warning (
507+ "A SteamBoilerPool instance was already created for steam_boiler_ids=%s "
508+ "and priority=%s using `microgrid.steam_boiler_pool(...)`."
509+ "\n Hint: If the multiple instances are created from the same actor, "
510+ "consider reusing the same instance."
511+ "\n Hint: If the instances are created from different actors, "
512+ "consider using different priorities to distinguish them." ,
513+ component_ids ,
514+ priority ,
515+ )
516+ else :
517+ self ._known_pool_keys .add (pool_key )
518+
519+ if ref_store_key not in self ._steam_boiler_pool_reference_stores :
520+ self ._steam_boiler_pool_reference_stores [ref_store_key ] = (
521+ SteamBoilerPoolReferenceStore (
522+ channel_registry = self ._channel_registry ,
523+ resampler_subscription_sender = self ._resampling_request_sender (),
524+ status_receiver = self ._steam_boiler_power_wrapper .status_channel .new_receiver (
525+ limit = 1
526+ ),
527+ power_manager_requests_sender = (
528+ self ._steam_boiler_power_wrapper .proposal_channel .new_sender ()
529+ ),
530+ power_manager_bounds_subs_sender = (
531+ self ._steam_boiler_power_wrapper .bounds_subscription_channel .new_sender ()
532+ ),
533+ power_distribution_results_fetcher = (
534+ self ._steam_boiler_power_wrapper .distribution_results_fetcher ()
535+ ),
536+ component_ids = component_ids ,
537+ )
538+ )
539+ return SteamBoilerPool (
540+ pool_ref_store = self ._steam_boiler_pool_reference_stores [ref_store_key ],
541+ name = name ,
542+ priority = priority ,
543+ )
544+
450545 def _data_sourcing_request_sender (self ) -> Sender [ComponentMetricRequest ]:
451546 """Return a Sender for sending requests to the data sourcing actor.
452547
@@ -503,12 +598,15 @@ async def _stop(self) -> None:
503598 await self ._battery_power_wrapper .stop ()
504599 await self ._ev_power_wrapper .stop ()
505600 await self ._pv_power_wrapper .stop ()
601+ await self ._steam_boiler_power_wrapper .stop ()
506602 for pool in self ._battery_pool_reference_stores .values ():
507603 await pool .stop ()
508604 for evpool in self ._ev_charger_pool_reference_stores .values ():
509605 await evpool .stop ()
510606 for pvpool in self ._pv_pool_reference_stores .values ():
511607 await pvpool .stop ()
608+ for steam_boiler_pool in self ._steam_boiler_pool_reference_stores .values ():
609+ await steam_boiler_pool .stop ()
512610
513611
514612_DATA_PIPELINE : _DataPipeline | None = None
@@ -682,6 +780,45 @@ def new_pv_pool(
682780 return _get ().new_pv_pool (priority = priority , component_ids = component_ids , name = name )
683781
684782
783+ def new_steam_boiler_pool (
784+ * ,
785+ priority : int ,
786+ component_ids : abc .Set [ComponentId ] | None = None ,
787+ name : str | None = None ,
788+ ) -> SteamBoilerPool :
789+ """Return a new `SteamBoilerPool` instance for the given parameters.
790+
791+ The priority value is used to resolve conflicts when multiple actors are trying to
792+ propose different power values for the same set of steam boilers.
793+
794+ !!! note
795+ When specifying priority, bigger values indicate higher priority.
796+
797+ It is recommended to reuse the same instance of the `SteamBoilerPool` within the same
798+ actor, unless they are managing different sets of steam boilers.
799+
800+ In deployments with multiple actors managing the same set of steam boilers, it is
801+ recommended to use different priorities to distinguish between them. If not,
802+ a random prioritization will be imposed on them to resolve conflicts, which may
803+ lead to unexpected behavior like longer duration to converge on the desired
804+ power.
805+
806+ Args:
807+ priority: The priority of the actor making the call.
808+ component_ids: Optional set of IDs of steam boilers to be managed by the
809+ `SteamBoilerPool`. If not specified, all steam boilers available in the component
810+ graph are used.
811+ name: An optional name used to identify this instance of the pool or a
812+ corresponding actor in the logs.
813+
814+ Returns:
815+ A `SteamBoilerPool` instance.
816+ """
817+ return _get ().new_steam_boiler_pool (
818+ priority = priority , component_ids = component_ids , name = name
819+ )
820+
821+
685822def grid () -> Grid :
686823 """Return the grid measuring point."""
687824 return _get ().grid ()
0 commit comments