Skip to content

Commit 82acdb9

Browse files
Add EV power and current streams to EVChargerPool (#201)
These formulas were getting generated from the component graph in the logical meter. Those have now been removed, and the formulas are now generated for the ev chargers that are part of the ev charger pool. Additional methods have been added to fetch the power and current values for individual ev chargers.
2 parents 70d9d43 + ee2e9bd commit 82acdb9

27 files changed

+859
-598
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""A formula engine for applying formulas."""
5+
from ._formula_engine import (
6+
FormulaEngine,
7+
FormulaEngine3Phase,
8+
FormulaReceiver,
9+
FormulaReceiver3Phase,
10+
_GenericEngine,
11+
_GenericFormulaReceiver,
12+
)
13+
from ._formula_engine_pool import FormulaEnginePool
14+
from ._resampled_formula_builder import ResampledFormulaBuilder
15+
16+
__all__ = [
17+
"FormulaEngine",
18+
"FormulaEngine3Phase",
19+
"FormulaReceiver",
20+
"FormulaReceiver3Phase",
21+
"FormulaEnginePool",
22+
"_GenericEngine",
23+
"_GenericFormulaReceiver",
24+
"ResampledFormulaBuilder",
25+
]

src/frequenz/sdk/timeseries/logical_meter/_exceptions.py renamed to src/frequenz/sdk/timeseries/_formula_engine/_exceptions.py

File renamed without changes.

src/frequenz/sdk/timeseries/logical_meter/_formula_engine.py renamed to src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py

File renamed without changes.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""A formula pool for helping with tracking running formula engines."""
5+
6+
from __future__ import annotations
7+
8+
from typing import TYPE_CHECKING, Type
9+
10+
from frequenz.channels import Sender
11+
12+
from ...actor import ChannelRegistry, ComponentMetricRequest
13+
from ...microgrid.component import ComponentMetricId
14+
from ._formula_generators._formula_generator import (
15+
FormulaGenerator,
16+
FormulaGeneratorConfig,
17+
)
18+
from ._resampled_formula_builder import ResampledFormulaBuilder
19+
20+
if TYPE_CHECKING:
21+
# Break circular import by enclosing these type hints in a `TYPE_CHECKING` block.
22+
from .._formula_engine import (
23+
FormulaReceiver,
24+
FormulaReceiver3Phase,
25+
_GenericEngine,
26+
_GenericFormulaReceiver,
27+
)
28+
29+
30+
class FormulaEnginePool:
31+
"""Creates and owns formula engines from string formulas, or formula generators.
32+
33+
If an engine already exists with a given name, it is reused instead.
34+
"""
35+
36+
def __init__(
37+
self,
38+
namespace: str,
39+
channel_registry: ChannelRegistry,
40+
resampler_subscription_sender: Sender[ComponentMetricRequest],
41+
) -> None:
42+
"""Create a new instance.
43+
44+
Args:
45+
namespace: namespace to use with the data pipeline.
46+
channel_registry: A channel registry instance shared with the resampling
47+
actor.
48+
resampler_subscription_sender: A sender for sending metric requests to the
49+
resampling actor.
50+
"""
51+
self._namespace = namespace
52+
self._channel_registry = channel_registry
53+
self._resampler_subscription_sender = resampler_subscription_sender
54+
self._engines: dict[str, "FormulaReceiver|FormulaReceiver3Phase"] = {}
55+
56+
async def from_string(
57+
self,
58+
formula: str,
59+
component_metric_id: ComponentMetricId,
60+
nones_are_zeros: bool = False,
61+
) -> "FormulaReceiver":
62+
"""Get a receiver for a manual formula.
63+
64+
Args:
65+
formula: formula to execute.
66+
component_metric_id: The metric ID to use when fetching receivers from the
67+
resampling actor.
68+
nones_are_zeros: Whether to treat None values from the stream as 0s. If
69+
False, the returned value will be a None.
70+
71+
Returns:
72+
A FormulaReceiver that streams values with the formulas applied.
73+
"""
74+
channel_key = formula + component_metric_id.value
75+
if channel_key in self._engines:
76+
return self._engines[channel_key].new_receiver()
77+
78+
builder = ResampledFormulaBuilder(
79+
self._namespace,
80+
formula,
81+
self._channel_registry,
82+
self._resampler_subscription_sender,
83+
component_metric_id,
84+
)
85+
formula_engine = await builder.from_string(formula, nones_are_zeros)
86+
self._engines[channel_key] = formula_engine
87+
88+
return formula_engine.new_receiver()
89+
90+
async def from_generator(
91+
self,
92+
channel_key: str,
93+
generator: "Type[FormulaGenerator[_GenericEngine]]",
94+
config: FormulaGeneratorConfig = FormulaGeneratorConfig(),
95+
) -> "_GenericFormulaReceiver":
96+
"""Get a receiver for a formula from a generator.
97+
98+
Args:
99+
channel_key: A string to uniquely identify the formula.
100+
generator: A formula generator.
101+
config: config to initialize the formula generator with.
102+
103+
Returns:
104+
A FormulaReceiver or a FormulaReceiver3Phase instance based on what the
105+
FormulaGenerator returns.
106+
"""
107+
if channel_key in self._engines:
108+
return self._engines[channel_key].new_receiver()
109+
110+
engine = await generator(
111+
self._namespace,
112+
self._channel_registry,
113+
self._resampler_subscription_sender,
114+
config,
115+
).generate()
116+
self._engines[channel_key] = engine
117+
return engine.new_receiver()

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/__init__.py renamed to src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ComponentNotFound,
1212
FormulaGenerationError,
1313
FormulaGenerator,
14+
FormulaGeneratorConfig,
1415
)
1516
from ._grid_current_formula import GridCurrentFormula
1617
from ._grid_power_formula import GridPowerFormula
@@ -21,6 +22,7 @@
2122
# Base class
2223
#
2324
"FormulaGenerator",
25+
"FormulaGeneratorConfig",
2426
#
2527
# Power Formula generators
2628
#

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/_battery_power_formula.py renamed to src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_battery_power_formula.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from .....sdk import microgrid
99
from ....microgrid.component import ComponentCategory, ComponentMetricId, InverterType
10-
from .._formula_engine import FormulaEngine
10+
from ..._formula_engine import FormulaEngine
1111
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
1212

1313
logger = logging.getLogger(__name__)

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/_battery_soc_formula.py renamed to src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_battery_soc_formula.py

File renamed without changes.

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/_ev_charger_current_formula.py renamed to src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_ev_charger_current_formula.py

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
"""Formula generator from component graph for 3-phase Grid Current."""
55

6+
from __future__ import annotations
7+
68
import logging
7-
from typing import List
89

9-
from .....sdk import microgrid
10-
from ....microgrid.component import Component, ComponentCategory, ComponentMetricId
10+
from ....microgrid.component import ComponentMetricId
1111
from .._formula_engine import FormulaEngine, FormulaEngine3Phase
1212
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
1313

@@ -18,21 +18,16 @@ class EVChargerCurrentFormula(FormulaGenerator):
1818
"""Create a formula engine from the component graph for calculating grid current."""
1919

2020
async def generate(self) -> FormulaEngine3Phase:
21-
"""Generate a formula for calculating total ev current from the component graph.
21+
"""Generate a formula for calculating total EV current for given component ids.
2222
2323
Returns:
24-
A formula engine that calculates total 3-phase ev charger current values.
24+
A formula engine that calculates total 3-phase EV Charger current values.
2525
"""
26-
component_graph = microgrid.get().component_graph
27-
ev_chargers = [
28-
comp
29-
for comp in component_graph.components()
30-
if comp.category == ComponentCategory.EV_CHARGER
31-
]
26+
component_ids = self._config.component_ids
3227

33-
if not ev_chargers:
28+
if not component_ids:
3429
logger.warning(
35-
"Unable to find any EV Chargers in the component graph. "
30+
"No EV Charger component IDs specified. "
3631
"Subscribing to the resampling actor with a non-existing "
3732
"component id, so that `0` values are sent from the formula."
3833
)
@@ -54,34 +49,34 @@ async def generate(self) -> FormulaEngine3Phase:
5449
(
5550
(
5651
await self._gen_phase_formula(
57-
ev_chargers, ComponentMetricId.CURRENT_PHASE_1
52+
component_ids, ComponentMetricId.CURRENT_PHASE_1
5853
)
5954
).new_receiver(),
6055
(
6156
await self._gen_phase_formula(
62-
ev_chargers, ComponentMetricId.CURRENT_PHASE_2
57+
component_ids, ComponentMetricId.CURRENT_PHASE_2
6358
)
6459
).new_receiver(),
6560
(
6661
await self._gen_phase_formula(
67-
ev_chargers, ComponentMetricId.CURRENT_PHASE_3
62+
component_ids, ComponentMetricId.CURRENT_PHASE_3
6863
)
6964
).new_receiver(),
7065
),
7166
)
7267

7368
async def _gen_phase_formula(
7469
self,
75-
ev_chargers: List[Component],
70+
component_ids: set[int],
7671
metric_id: ComponentMetricId,
7772
) -> FormulaEngine:
7873
builder = self._get_builder("ev-current", metric_id)
7974

80-
# generate a formula that just adds values from all ev-chargers.
81-
for idx, comp in enumerate(ev_chargers):
75+
# generate a formula that just adds values from all EV Chargers.
76+
for idx, component_id in enumerate(component_ids):
8277
if idx > 0:
8378
builder.push_oper("+")
8479

85-
await builder.push_component_metric(comp.component_id, nones_are_zeros=True)
80+
await builder.push_component_metric(component_id, nones_are_zeros=True)
8681

8782
return builder.build()

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/_ev_charger_power_formula.py renamed to src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_ev_charger_power_formula.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55

66
import logging
77

8-
from .....sdk import microgrid
9-
from ....microgrid.component import ComponentCategory, ComponentMetricId
8+
from ....microgrid.component import ComponentMetricId
109
from .._formula_engine import FormulaEngine
1110
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
1211

@@ -17,22 +16,17 @@ class EVChargerPowerFormula(FormulaGenerator):
1716
"""Create a formula engine from the component graph for calculating grid power."""
1817

1918
async def generate(self) -> FormulaEngine:
20-
"""Generate a formula for calculating total EV power from the component graph.
19+
"""Generate a formula for calculating total EV power for given component ids.
2120
2221
Returns:
23-
A formula engine that calculates total EV charger power values.
22+
A formula engine that calculates total EV Charger power values.
2423
"""
2524
builder = self._get_builder("ev-power", ComponentMetricId.ACTIVE_POWER)
26-
component_graph = microgrid.get().component_graph
27-
ev_chargers = [
28-
comp
29-
for comp in component_graph.components()
30-
if comp.category == ComponentCategory.EV_CHARGER
31-
]
32-
33-
if not ev_chargers:
25+
26+
component_ids = self._config.component_ids
27+
if not component_ids:
3428
logger.warning(
35-
"Unable to find any EV Chargers in the component graph. "
29+
"No EV Charger component IDs specified. "
3630
"Subscribing to the resampling actor with a non-existing "
3731
"component id, so that `0` values are sent from the formula."
3832
)
@@ -44,10 +38,10 @@ async def generate(self) -> FormulaEngine:
4438
)
4539
return builder.build()
4640

47-
for idx, comp in enumerate(ev_chargers):
41+
for idx, component_id in enumerate(component_ids):
4842
if idx > 0:
4943
builder.push_oper("+")
5044

51-
await builder.push_component_metric(comp.component_id, nones_are_zeros=True)
45+
await builder.push_component_metric(component_id, nones_are_zeros=True)
5246

5347
return builder.build()

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/_formula_generator.py renamed to src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_formula_generator.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
"""Base class for formula generators that use the component graphs."""
55

6+
from __future__ import annotations
7+
68
import sys
79
from abc import ABC, abstractmethod
10+
from dataclasses import dataclass
811
from typing import Generic
912

1013
from frequenz.channels import Sender
@@ -26,6 +29,13 @@ class ComponentNotFound(FormulaGenerationError):
2629
NON_EXISTING_COMPONENT_ID = sys.maxsize
2730

2831

32+
@dataclass(frozen=True)
33+
class FormulaGeneratorConfig:
34+
"""Config for formula generators."""
35+
36+
component_ids: set[int] | None = None
37+
38+
2939
class FormulaGenerator(ABC, Generic[_GenericEngine]):
3040
"""A class for generating formulas from the component graph."""
3141

@@ -34,6 +44,7 @@ def __init__(
3444
namespace: str,
3545
channel_registry: ChannelRegistry,
3646
resampler_subscription_sender: Sender[ComponentMetricRequest],
47+
config: FormulaGeneratorConfig,
3748
) -> None:
3849
"""Create a `FormulaGenerator` instance.
3950
@@ -43,10 +54,12 @@ def __init__(
4354
actor.
4455
resampler_subscription_sender: A sender for sending metric requests to the
4556
resampling actor.
57+
config: configs for the formula generator.
4658
"""
4759
self._channel_registry = channel_registry
4860
self._resampler_subscription_sender = resampler_subscription_sender
4961
self._namespace = namespace
62+
self._config = config
5063

5164
def _get_builder(
5265
self, name: str, component_metric_id: ComponentMetricId

0 commit comments

Comments
 (0)