33
44"""Formula generator from component graph for Grid Power."""
55
6+ import itertools
67import logging
78
8- from frequenz .client .microgrid import ComponentMetricId
9+ from frequenz .client .microgrid import Component , ComponentCategory , ComponentMetricId
910
1011from ....microgrid import connection_manager
1112from ..._quantities import Power
1213from ...formula_engine import FormulaEngine
14+ from ._fallback_formula_metric_fetcher import FallbackFormulaMetricFetcher
1315from ._formula_generator import (
1416 NON_EXISTING_COMPONENT_ID ,
1517 ComponentNotFound ,
1618 FormulaGenerationError ,
1719 FormulaGenerator ,
20+ FormulaGeneratorConfig ,
1821)
1922
2023_logger = logging .getLogger (__name__ )
@@ -48,6 +51,7 @@ def generate(
4851 builder = self ._get_builder (
4952 "battery-power" , ComponentMetricId .ACTIVE_POWER , Power .from_watts
5053 )
54+
5155 component_ids = self ._config .component_ids
5256 if not component_ids :
5357 _logger .warning (
@@ -65,39 +69,106 @@ def generate(
6569
6670 component_graph = connection_manager .get ().component_graph
6771
68- battery_inverters = frozenset (
69- frozenset (
72+ # all_connected_batteries has to be set because
73+ # multiple inverters can be connected to the same battery
74+ inv_bat_mapping : dict [Component , set [Component ]] = {}
75+ all_connected_batteries : set [Component ] = set ()
76+ for bat_id in component_ids :
77+ inverters = set (
7078 filter (
7179 component_graph .is_battery_inverter ,
7280 component_graph .predecessors (bat_id ),
7381 )
7482 )
75- for bat_id in component_ids
76- )
77-
78- if not all (battery_inverters ):
79- raise ComponentNotFound (
80- "All batteries must have at least one inverter as a predecessor."
81- )
83+ if len (inverters ) == 0 :
84+ raise ComponentNotFound (
85+ "All batteries must have at least one inverter as a predecessor." ,
86+ "Battery ID %s has no inverter as a predecessor." ,
87+ bat_id ,
88+ )
8289
83- all_connected_batteries = set ()
84- for inverters in battery_inverters :
8590 for inverter in inverters :
86- all_connected_batteries . update (
87- component_graph . successors ( inverter .component_id )
91+ inv_bat_mapping [ inverter ] = component_graph . successors (
92+ inverter .component_id
8893 )
94+ all_connected_batteries .update (inv_bat_mapping [inverter ])
8995
9096 if len (all_connected_batteries ) != len (component_ids ):
9197 raise FormulaGenerationError (
9298 "All batteries behind a set of inverters must be requested."
9399 )
94100
95- # Iterate over the flattened list of inverters
96- for idx , comp in enumerate (
97- inverter for inverters in battery_inverters for inverter in inverters
98- ):
99- if idx > 0 :
100- builder .push_oper ("+" )
101- builder .push_component_metric (comp .component_id , nones_are_zeros = True )
101+ if self ._config .allow_fallback :
102+ fallbacks = self ._get_fallback_formulas (inv_bat_mapping )
103+
104+ for idx , (primary_component , fallback_formula ) in enumerate (
105+ fallbacks .items ()
106+ ):
107+ if idx > 0 :
108+ builder .push_oper ("+" )
109+
110+ builder .push_component_metric (
111+ primary_component .component_id ,
112+ nones_are_zeros = (
113+ primary_component .category != ComponentCategory .METER
114+ ),
115+ fallback = fallback_formula ,
116+ )
117+ else :
118+ for idx , comp in enumerate (inv_bat_mapping .keys ()):
119+ if idx > 0 :
120+ builder .push_oper ("+" )
121+ builder .push_component_metric (comp .component_id , nones_are_zeros = True )
102122
103123 return builder .build ()
124+
125+ def _get_fallback_formulas (
126+ self , inv_bat_mapping : dict [Component , set [Component ]]
127+ ) -> dict [Component , FallbackFormulaMetricFetcher [Power ] | None ]:
128+ """Find primary and fallback components and create fallback formulas.
129+
130+ The primary component is the one that will be used to calculate the battery power.
131+ But if it is not available, the fallback formula will be used instead.
132+ Fallback formulas calculates the battery power using the fallback components.
133+ Fallback formulas are wrapped in `FallbackFormulaMetricFetcher`.
134+
135+ Args:
136+ inv_bat_mapping: A mapping from inverter to connected batteries.
137+
138+ Returns:
139+ A dictionary mapping primary components to their FallbackFormulaMetricFetcher.
140+ """
141+ fallbacks = self ._get_metric_fallback_components (set (inv_bat_mapping .keys ()))
142+
143+ fallback_formulas : dict [
144+ Component , FallbackFormulaMetricFetcher [Power ] | None
145+ ] = {}
146+ for primary_component , fallback_components in fallbacks .items ():
147+ if len (fallback_components ) == 0 :
148+ fallback_formulas [primary_component ] = None
149+ continue
150+
151+ battery_ids = set (
152+ map (
153+ lambda battery : battery .component_id ,
154+ itertools .chain .from_iterable (
155+ inv_bat_mapping [inv ] for inv in fallback_components
156+ ),
157+ )
158+ )
159+
160+ generator = BatteryPowerFormula (
161+ f"{ self ._namespace } _fallback_{ battery_ids } " ,
162+ self ._channel_registry ,
163+ self ._resampler_subscription_sender ,
164+ FormulaGeneratorConfig (
165+ component_ids = battery_ids ,
166+ allow_fallback = False ,
167+ ),
168+ )
169+
170+ fallback_formulas [primary_component ] = FallbackFormulaMetricFetcher (
171+ generator
172+ )
173+
174+ return fallback_formulas
0 commit comments