Skip to content

Commit ae1c2a0

Browse files
authored
Add microgrid.frequency() (#579)
fixes #341
2 parents 8da8583 + 5f694aa commit ae1c2a0

File tree

13 files changed

+455
-49
lines changed

13 files changed

+455
-49
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
Note that a microgrid is allowed to have zero or one grid connection point. Microgrids configured as islands will have zero grid connection points, and microgrids configured as grid-connected will have one grid connection point.
2020

21+
- A new method `microgrid.frequeny()` was added to allow easy access to the current frequency of the grid.
22+
2123
- A new class `Fuse` has been added to represent fuses. This class has a member variable `max_current` which represents the maximum current that can course through the fuse. If the current flowing through a fuse is greater than this limit, then the fuse will break the circuit.
2224

2325

src/frequenz/sdk/actor/_data_sourcing/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
"""The DataSourcingActor."""
55

6+
from ._component_metric_request import ComponentMetricRequest
67
from .data_sourcing import DataSourcingActor
7-
from .microgrid_api_source import ComponentMetricRequest
88

99
__all__ = [
1010
"ComponentMetricRequest",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""The ComponentMetricRequest class."""
5+
6+
from dataclasses import dataclass
7+
from datetime import datetime
8+
9+
from ...microgrid.component._component import ComponentMetricId
10+
11+
12+
@dataclass
13+
class ComponentMetricRequest:
14+
"""A request object to start streaming a metric for a component."""
15+
16+
namespace: str
17+
"""The namespace that this request belongs to.
18+
19+
Metric requests with a shared namespace enable the reuse of channels within
20+
that namespace.
21+
22+
If for example, an actor making a multiple requests, uses the name of the
23+
actor as the namespace, then requests from the actor will get reused when
24+
possible.
25+
"""
26+
27+
component_id: int
28+
"""The ID of the requested component."""
29+
30+
metric_id: ComponentMetricId
31+
"""The ID of the requested component's metric."""
32+
33+
start_time: datetime | None
34+
"""The start time from which data is required.
35+
36+
When None, we will stream only live data.
37+
"""
38+
39+
def get_channel_name(self) -> str:
40+
"""Return a channel name constructed from Self.
41+
42+
This channel name can be used by the sending side and receiving sides to
43+
identify the right channel from the ChannelRegistry.
44+
45+
Returns:
46+
A string denoting a channel name.
47+
"""
48+
return (
49+
f"component-stream::{self.component_id}::{self.metric_id.name}::"
50+
f"{self.start_time}::{self.namespace}"
51+
)

src/frequenz/sdk/actor/_data_sourcing/data_sourcing.py

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

88
from .._actor import Actor
99
from .._channel_registry import ChannelRegistry
10-
from .microgrid_api_source import ComponentMetricRequest, MicrogridApiSource
10+
from ._component_metric_request import ComponentMetricRequest
11+
from .microgrid_api_source import MicrogridApiSource
1112

1213

1314
class DataSourcingActor(Actor):

src/frequenz/sdk/actor/_data_sourcing/microgrid_api_source.py

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
import asyncio
77
import logging
8-
from dataclasses import dataclass
9-
from datetime import datetime
108
from typing import Any, Callable, Dict, List, Optional, Tuple
119

1210
from frequenz.channels import Receiver, Sender
@@ -23,49 +21,7 @@
2321
from ...timeseries import Sample
2422
from ...timeseries._quantities import Quantity
2523
from .._channel_registry import ChannelRegistry
26-
27-
28-
@dataclass
29-
class ComponentMetricRequest:
30-
"""A request object to start streaming a metric for a component."""
31-
32-
namespace: str
33-
"""The namespace that this request belongs to.
34-
35-
Metric requests with a shared namespace enable the reuse of channels within
36-
that namespace.
37-
38-
If for example, an actor making a multiple requests, uses the name of the
39-
actor as the namespace, then requests from the actor will get reused when
40-
possible.
41-
"""
42-
43-
component_id: int
44-
"""The ID of the requested component."""
45-
46-
metric_id: ComponentMetricId
47-
"""The ID of the requested component's metric."""
48-
49-
start_time: Optional[datetime]
50-
"""The start time from which data is required.
51-
52-
When None, we will stream only live data.
53-
"""
54-
55-
def get_channel_name(self) -> str:
56-
"""Return a channel name constructed from Self.
57-
58-
This channel name can be used by the sending side and receiving sides to
59-
identify the right channel from the ChannelRegistry.
60-
61-
Returns:
62-
A string denoting a channel name.
63-
"""
64-
return (
65-
f"component-stream::{self.component_id}::{self.metric_id.name}::"
66-
f"{self.start_time}::{self.namespace}"
67-
)
68-
24+
from ._component_metric_request import ComponentMetricRequest
6925

7026
_MeterDataMethods: Dict[ComponentMetricId, Callable[[MeterData], float]] = {
7127
ComponentMetricId.ACTIVE_POWER: lambda msg: msg.active_power,
@@ -75,6 +31,7 @@ def get_channel_name(self) -> str:
7531
ComponentMetricId.VOLTAGE_PHASE_1: lambda msg: msg.voltage_per_phase[0],
7632
ComponentMetricId.VOLTAGE_PHASE_2: lambda msg: msg.voltage_per_phase[1],
7733
ComponentMetricId.VOLTAGE_PHASE_3: lambda msg: msg.voltage_per_phase[2],
34+
ComponentMetricId.FREQUENCY: lambda msg: msg.frequency,
7835
}
7936

8037
_BatteryDataMethods: Dict[ComponentMetricId, Callable[[BatteryData], float]] = {
@@ -111,6 +68,7 @@ def get_channel_name(self) -> str:
11168
ComponentMetricId.ACTIVE_POWER_INCLUSION_UPPER_BOUND: lambda msg: (
11269
msg.active_power_inclusion_upper_bound
11370
),
71+
ComponentMetricId.FREQUENCY: lambda msg: msg.frequency,
11472
}
11573

11674
_EVChargerDataMethods: Dict[ComponentMetricId, Callable[[EVChargerData], float]] = {
@@ -121,6 +79,7 @@ def get_channel_name(self) -> str:
12179
ComponentMetricId.VOLTAGE_PHASE_1: lambda msg: msg.voltage_per_phase[0],
12280
ComponentMetricId.VOLTAGE_PHASE_2: lambda msg: msg.voltage_per_phase[1],
12381
ComponentMetricId.VOLTAGE_PHASE_3: lambda msg: msg.voltage_per_phase[2],
82+
ComponentMetricId.FREQUENCY: lambda msg: msg.frequency,
12483
}
12584

12685

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from ..actor import ResamplerConfig
1111
from . import _data_pipeline, client, component, connection_manager, fuse, grid
12-
from ._data_pipeline import battery_pool, ev_charger_pool, logical_meter
12+
from ._data_pipeline import battery_pool, ev_charger_pool, frequency, logical_meter
1313
from ._graph import ComponentGraph
1414

1515

@@ -39,5 +39,6 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) ->
3939
"ev_charger_pool",
4040
"fuse",
4141
"grid",
42+
"frequency",
4243
"logical_meter",
4344
]

src/frequenz/sdk/microgrid/_data_pipeline.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
from frequenz.channels import Broadcast, Sender
1919

20+
from ..microgrid.component import Component
21+
from ..timeseries._grid_frequency import GridFrequency
2022
from . import connection_manager
2123
from .component import ComponentCategory
2224

@@ -105,6 +107,29 @@ def __init__(
105107
self._logical_meter: "LogicalMeter" | None = None
106108
self._ev_charger_pools: dict[frozenset[int], "EVChargerPool"] = {}
107109
self._battery_pools: dict[frozenset[int], "BatteryPool"] = {}
110+
self._frequency_pool: dict[int, GridFrequency] = {}
111+
112+
def frequency(self, component: Component | None = None) -> GridFrequency:
113+
"""Fetch the grid frequency for the microgrid.
114+
115+
Args:
116+
component: The component to use when fetching the grid frequency. If None,
117+
the component will be fetched from the registry.
118+
119+
Returns:
120+
A GridFrequency instance.
121+
"""
122+
if component is None:
123+
component = GridFrequency.find_frequency_component()
124+
125+
if component.component_id in self._frequency_pool:
126+
return self._frequency_pool[component.component_id]
127+
128+
grid_frequency = GridFrequency(
129+
self._data_sourcing_request_sender(), self._channel_registry, component
130+
)
131+
self._frequency_pool[component.component_id] = grid_frequency
132+
return grid_frequency
108133

109134
def logical_meter(self) -> LogicalMeter:
110135
"""Return the logical meter instance.
@@ -300,6 +325,19 @@ async def initialize(resampler_config: ResamplerConfig) -> None:
300325
_DATA_PIPELINE = _DataPipeline(resampler_config)
301326

302327

328+
def frequency(component: Component | None = None) -> GridFrequency:
329+
"""Return the grid frequency.
330+
331+
Args:
332+
component: Optional component to get the frequency for. If not specified,
333+
the frequency of the grid is returned.
334+
335+
Returns:
336+
The grid frequency.
337+
"""
338+
return _get().frequency(component)
339+
340+
303341
def logical_meter() -> LogicalMeter:
304342
"""Return the logical meter instance.
305343

src/frequenz/sdk/microgrid/component/_component.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ class ComponentMetricId(Enum):
206206
VOLTAGE_PHASE_3 = "voltage_phase_3"
207207
"""Voltage in phase 3."""
208208

209+
FREQUENCY = "frequency"
210+
209211
SOC = "soc"
210212
"""State of charge."""
211213
SOC_LOWER_BOUND = "soc_lower_bound"

src/frequenz/sdk/microgrid/component/_component_data.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ class InverterData(ComponentData):
278278
"""
279279
# pylint: enable=line-too-long
280280

281+
frequency: float
282+
"""AC frequency, in Hertz (Hz)."""
283+
281284
_component_state: inverter_pb.ComponentState.ValueType
282285
"""State of the inverter."""
283286

@@ -303,6 +306,7 @@ def from_proto(cls, raw: microgrid_pb.ComponentData) -> InverterData:
303306
active_power_exclusion_lower_bound=raw_power.system_exclusion_bounds.lower,
304307
active_power_inclusion_upper_bound=raw_power.system_inclusion_bounds.upper,
305308
active_power_exclusion_upper_bound=raw_power.system_exclusion_bounds.upper,
309+
frequency=raw.inverter.data.ac.frequency.value,
306310
_component_state=raw.inverter.state.component_state,
307311
_errors=list(raw.inverter.errors),
308312
)
@@ -332,6 +336,9 @@ class EVChargerData(ComponentData):
332336
wire for phase/line 1,2 and 3 respectively.
333337
"""
334338

339+
frequency: float
340+
"""AC frequency, in Hertz (Hz)."""
341+
335342
cable_state: EVChargerCableState
336343
"""The state of the ev charger's cable."""
337344

@@ -366,6 +373,7 @@ def from_proto(cls, raw: microgrid_pb.ComponentData) -> EVChargerData:
366373
component_state=EVChargerComponentState.from_pb(
367374
raw.ev_charger.state.component_state
368375
),
376+
frequency=raw.ev_charger.data.ac.frequency.value,
369377
)
370378
ev_charger_data._set_raw(raw=raw)
371379
return ev_charger_data

0 commit comments

Comments
 (0)