Skip to content

Commit a6db7c5

Browse files
authored
Merge pull request #21 from cyiallou/feat/filter_component_states
Add state duration extraction and alert filtering
2 parents 246a467 + 4080b32 commit a6db7c5

File tree

3 files changed

+412
-0
lines changed

3 files changed

+412
-0
lines changed

RELEASE_NOTES.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
## New Features
1313

1414
* Add Readme information
15+
* Added functionality to extract state durations and filter alerts.
1516

1617
## Bug Fixes
1718

src/frequenz/reporting/_reporting.py

+218
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55

66
from collections import namedtuple
77
from datetime import datetime, timedelta
8+
from itertools import groupby
9+
from typing import Any
810

911
from frequenz.client.common.metric import Metric
1012
from frequenz.client.reporting import ReportingApiClient
13+
from frequenz.client.reporting._client import MetricSample
1114

1215
CumulativeEnergy = namedtuple(
1316
"CumulativeEnergy", ["start_time", "end_time", "consumption", "production"]
@@ -122,3 +125,218 @@ async def cumulative_energy(
122125
consumption=consumption,
123126
production=production,
124127
)
128+
129+
130+
# pylint: disable-next=too-many-arguments
131+
async def fetch_and_extract_state_durations(
132+
*,
133+
client: ReportingApiClient,
134+
microgrid_components: list[tuple[int, list[int]]],
135+
metrics: list[Metric],
136+
start_time: datetime,
137+
end_time: datetime,
138+
resampling_period: timedelta | None,
139+
alert_states: list[int],
140+
include_warnings: bool = True,
141+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
142+
"""Fetch data using the Reporting API and extract state durations and alert records.
143+
144+
Args:
145+
client: The client used to fetch the metric samples from the Reporting API.
146+
microgrid_components: List of tuples where each tuple contains microgrid
147+
ID and corresponding component IDs.
148+
metrics: List of metric names.
149+
NOTE: The service will support requesting states without metrics in
150+
the future and this argument will be removed.
151+
start_time: The start date and time for the period.
152+
end_time: The end date and time for the period.
153+
resampling_period: The period for resampling the data. If None, data
154+
will be returned in its original resolution
155+
alert_states: List of component state values that should trigger an alert.
156+
include_warnings: Whether to include warning state values in the alert
157+
records.
158+
159+
Returns:
160+
A tuple containing two lists:
161+
- all_states: Contains all state records including start and end times.
162+
- alert_records: Contains filtered records matching the alert criteria.
163+
"""
164+
samples = await _fetch_component_data(
165+
client=client,
166+
microgrid_components=microgrid_components,
167+
metrics=metrics,
168+
start_time=start_time,
169+
end_time=end_time,
170+
resampling_period=resampling_period,
171+
include_states=True,
172+
include_bounds=False,
173+
)
174+
175+
all_states, alert_records = extract_state_durations(
176+
samples, alert_states, include_warnings
177+
)
178+
return all_states, alert_records
179+
180+
181+
def extract_state_durations(
182+
samples: list[MetricSample],
183+
alert_states: list[int],
184+
include_warnings: bool = True,
185+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
186+
"""
187+
Extract state durations and alert records based on state transitions.
188+
189+
Args:
190+
samples: List of MetricSample instances containing the reporting data.
191+
alert_states: List of component state values that should trigger an alert.
192+
Component error codes are reported by default.
193+
include_warnings: Whether to include warning state values in the alert records.
194+
195+
Returns:
196+
A tuple containing two lists:
197+
- all_states: Contains all state records including start and end times.
198+
- alert_records: Contains filtered records matching the alert criteria.
199+
"""
200+
alert_metrics = ["warning", "error"] if include_warnings else ["error"]
201+
state_metrics = ["state"] + alert_metrics
202+
filtered_samples = sorted(
203+
(s for s in samples if s.metric in state_metrics),
204+
key=lambda s: (s.microgrid_id, s.component_id, s.metric, s.timestamp),
205+
)
206+
207+
if not filtered_samples:
208+
return [], []
209+
210+
# Group samples by (microgrid_id, component_id, metric)
211+
all_states = []
212+
for key, group in groupby(
213+
filtered_samples, key=lambda s: (s.microgrid_id, s.component_id, s.metric)
214+
):
215+
states = _process_group_samples(key, list(group))
216+
all_states.extend(states)
217+
218+
all_states.sort(
219+
key=lambda x: (x["microgrid_id"], x["component_id"], x["start_time"])
220+
)
221+
222+
alert_records = _filter_alerts(all_states, alert_states, alert_metrics)
223+
return all_states, alert_records
224+
225+
226+
def _process_group_samples(
227+
key: tuple[int, int, str],
228+
group_samples: list["MetricSample"],
229+
) -> list[dict[str, Any]]:
230+
"""Process samples for a single group to extract state durations.
231+
232+
Args:
233+
key: Tuple containing microgrid ID, component ID, and metric.
234+
group_samples: List of samples for the group.
235+
236+
Returns:
237+
List of state records.
238+
"""
239+
mid, cid, metric = key
240+
state_records = []
241+
current_state_value = None
242+
start_time = None
243+
244+
for sample in group_samples:
245+
if current_state_value != sample.value:
246+
# Close previous state run
247+
if current_state_value is not None:
248+
state_records.append(
249+
{
250+
"microgrid_id": mid,
251+
"component_id": cid,
252+
"state_type": metric,
253+
"state_value": current_state_value,
254+
"start_time": start_time,
255+
"end_time": sample.timestamp,
256+
}
257+
)
258+
# Start new state run
259+
current_state_value = sample.value
260+
start_time = sample.timestamp
261+
262+
# Close the last state run
263+
state_records.append(
264+
{
265+
"microgrid_id": mid,
266+
"component_id": cid,
267+
"state_type": metric,
268+
"state_value": current_state_value,
269+
"start_time": start_time,
270+
"end_time": None,
271+
}
272+
)
273+
274+
return state_records
275+
276+
277+
def _filter_alerts(
278+
all_states: list[dict[str, Any]],
279+
alert_states: list[int],
280+
alert_metrics: list[str],
281+
) -> list[dict[str, Any]]:
282+
"""Identify alert records from all states.
283+
284+
Args:
285+
all_states: List of all state records.
286+
alert_states: List of component state values that should trigger an alert.
287+
alert_metrics: List of metric names that should trigger an alert.
288+
289+
Returns:
290+
List of alert records.
291+
"""
292+
return [
293+
state
294+
for state in all_states
295+
if (
296+
(state["state_type"] == "state" and state["state_value"] in alert_states)
297+
or (state["state_type"] in alert_metrics)
298+
)
299+
]
300+
301+
302+
# pylint: disable-next=too-many-arguments
303+
async def _fetch_component_data(
304+
*,
305+
client: ReportingApiClient,
306+
microgrid_components: list[tuple[int, list[int]]],
307+
metrics: list[Metric],
308+
start_time: datetime,
309+
end_time: datetime,
310+
resampling_period: timedelta | None,
311+
include_states: bool = False,
312+
include_bounds: bool = False,
313+
) -> list[MetricSample]:
314+
"""Fetch component data from the Reporting API.
315+
316+
Args:
317+
client: The client used to fetch the metric samples from the Reporting API.
318+
microgrid_components: List of tuples where each tuple contains
319+
microgrid ID and corresponding component IDs.
320+
metrics: List of metric names.
321+
start_time: The start date and time for the period.
322+
end_time: The end date and time for the period.
323+
resampling_period: The period for resampling the data. If None, data
324+
will be returned in its original resolution
325+
include_states: Whether to include the state data.
326+
include_bounds: Whether to include the bound data.
327+
328+
Returns:
329+
List of MetricSample instances containing the reporting data.
330+
"""
331+
return [
332+
sample
333+
async for sample in client.list_microgrid_components_data(
334+
microgrid_components=microgrid_components,
335+
metrics=metrics,
336+
start_dt=start_time,
337+
end_dt=end_time,
338+
resampling_period=resampling_period,
339+
include_states=include_states,
340+
include_bounds=include_bounds,
341+
)
342+
]

0 commit comments

Comments
 (0)