Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "frequenz-microgrid"
version = "0.3.0"
version = "0.4.0"
edition = "2024"
description = "A high-level interface to the Frequenz Microgrid API."
repository = "https://github.com/frequenz-floss/frequenz-microgrid-rs"
Expand All @@ -13,7 +13,7 @@ path = "src/lib.rs"
[dependencies]
async-trait = "0.1.89"
chrono = "0.4"
frequenz-microgrid-component-graph = "0.2.0"
frequenz-microgrid-component-graph = "0.5.0"
frequenz-microgrid-formula-engine = "0.1.0"
frequenz-resampling = "0.2"
futures = "0.3.31"
Expand Down
123 changes: 117 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,125 @@
[<img alt="docs.rs" src="https://img.shields.io/docsrs/frequenz-microgrid">](https://docs.rs/frequenz-microgrid)
[<img alt="Crates.io" src="https://img.shields.io/crates/v/frequenz-microgrid">](https://crates.io/crates/frequenz-microgrid)

High-level interface for Rust to the Frequenz Microgrid API.
High-level Rust interface for the Frequenz Microgrid API.

## Documentation
The crate connects to a Microgrid API server, builds a [`ComponentGraph`]
from the live topology, and exposes typed, formula-driven streams of
microgrid metrics — grid power, battery state-of-charge, PV reactive
power, consumer current, and so on — without requiring callers to write
the per-component formulas by hand.

For more information, please visit the [documentation
website](https://docs.rs/frequenz-microgrid).
Support for controlling components is coming soon.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Announcements like this one usually don't age well, in my experience. And why ourselves under pressure? 😏

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, because microgrid-rs is supposed to read and control. This lets them know that it is not there yet. Also, this is the next big thing for me, multiple things are blocked until we get this.


## Quick start

```toml
[dependencies]
frequenz-microgrid = "0.4"
chrono = "0.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

Stream the grid's active power once per second:

```rust , ignore
use chrono::TimeDelta;
use frequenz_microgrid::{Error, LogicalMeterConfig, Microgrid, metric};

#[tokio::main]
async fn main() -> Result<(), Error> {
let microgrid = Microgrid::try_new(
"http://[::1]:8800",
LogicalMeterConfig::new(TimeDelta::try_seconds(1).unwrap()),
)
.await?;

let mut grid = microgrid
.logical_meter()
.grid::<metric::AcPowerActive>()?
.subscribe()
.await?;

while let Ok(sample) = grid.recv().await {
println!("{:?}: {:?}", sample.timestamp(), sample.value());
}
Ok(())
}
```

[`Microgrid::try_new`] blocks (with retries) until the server is
reachable and returns a graph that builds successfully, so applications
can start before their backing service is ready.

## Testing with the in-crate mock

[`frequenz_microgrid::test_utils`][`client::test_utils`] ships a
[`MockMicrogridApiClient`] (plus [`MockComponent`] and
[`TokioSyncedClock`] helpers) for downstream tests. Enable it as a
dev-dependency feature:

```toml
[dev-dependencies]
frequenz-microgrid = { version = "0.4", features = ["test-utils"] }
```

## What's included

- [`Microgrid`] / [`LogicalMeterHandle`]: typed formulas for [`grid`],
[`battery`], [`pv`], [`chp`], [`ev_charger`], [`consumer`],
[`producer`], and individual [`component`]s, parametrised over a
metric in [`metric`].
- [`BatteryPool`]: aggregated active-power bounds and state-of-charge
for one or more batteries.
- [`MicrogridClientHandle`]: cloneable low-level gRPC handle with
per-stream automatic reconnect.
- [`quantity`]: [`Power`], [`Current`], [`Voltage`], [`ReactivePower`],
[`Energy`], [`Frequency`], [`Percentage`]. Unit conversions are
explicit at every API surface.

## Configuring the underlying graph

[`LogicalMeterConfig::with_component_graph_config`] forwards a
[`ComponentGraphConfig`] to the
[`frequenz-microgrid-component-graph`](https://docs.rs/frequenz-microgrid-component-graph)
builder, exposing knobs like
[`prefer_meters_in_component_formulas`],
[`include_phantom_loads_in_consumer_formula`], and per-formula
overrides. If not set, the graph crate's `Default::default()` is used.

## Contributing

If you want to know how to build this project and contribute to it, please
check out the [Contributing Guide](CONTRIBUTING.md).
See the [Contributing Guide](https://github.com/frequenz-floss/frequenz-microgrid-rs/blob/HEAD/CONTRIBUTING.md).

[`ComponentGraph`]: https://docs.rs/frequenz-microgrid-component-graph/latest/frequenz_microgrid_component_graph/struct.ComponentGraph.html
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this list of docs links about? It does not look related to "Contributing", the previous headline ...?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, they're the link references. So all occurrences of [ComponentGraph] in the text above will point to this link.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And don't show up in the rendered markdown.

[`ComponentGraphConfig`]: https://docs.rs/frequenz-microgrid-component-graph/latest/frequenz_microgrid_component_graph/struct.ComponentGraphConfig.html
[`prefer_meters_in_component_formulas`]: https://docs.rs/frequenz-microgrid-component-graph/latest/frequenz_microgrid_component_graph/struct.ComponentGraphConfigBuilder.html#method.prefer_meters_in_component_formulas
[`include_phantom_loads_in_consumer_formula`]: https://docs.rs/frequenz-microgrid-component-graph/latest/frequenz_microgrid_component_graph/struct.ComponentGraphConfigBuilder.html#method.include_phantom_loads_in_consumer_formula
[`Microgrid`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.Microgrid.html
[`Microgrid::try_new`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.Microgrid.html#method.try_new
[`LogicalMeterHandle`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html
[`LogicalMeterConfig`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterConfig.html
[`LogicalMeterConfig::with_component_graph_config`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterConfig.html#method.with_component_graph_config
[`BatteryPool`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.BatteryPool.html
[`MicrogridClientHandle`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.MicrogridClientHandle.html
[`grid`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html#method.grid
[`battery`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html#method.battery
[`pv`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html#method.pv
[`chp`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html#method.chp
[`ev_charger`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html#method.ev_charger
[`consumer`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html#method.consumer
[`producer`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html#method.producer
[`component`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/struct.LogicalMeterHandle.html#method.component
[`metric`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/metric/index.html
[`quantity`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/quantity/index.html
[`Power`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/quantity/struct.Power.html
[`Current`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/quantity/struct.Current.html
[`Voltage`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/quantity/struct.Voltage.html
[`ReactivePower`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/quantity/struct.ReactivePower.html
[`Energy`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/quantity/struct.Energy.html
[`Frequency`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/quantity/struct.Frequency.html
[`Percentage`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/quantity/struct.Percentage.html
[`client::test_utils`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/client/test_utils/index.html
[`MockMicrogridApiClient`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/client/test_utils/struct.MockMicrogridApiClient.html
[`MockComponent`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/client/test_utils/struct.MockComponent.html
[`TokioSyncedClock`]: https://docs.rs/frequenz-microgrid/latest/frequenz_microgrid/client/test_utils/struct.TokioSyncedClock.html
15 changes: 8 additions & 7 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Frequenz Microgrid Release Notes

## Summary
## Upgrading

<!-- Here goes a general summary of what this release is about -->
- This updates the component-graph version to 0.5, which treats unhandled component categories as pass-through.

## Upgrading
- The component-graph configuration is no longer hardcoded. `LogicalMeterConfig` now carries a `ComponentGraphConfig` (set via `with_component_graph_config`), defaulting to `ComponentGraphConfig::default()` from the graph crate. This is a behavior change for callers that relied on the previously hardcoded values: the old defaults were `allow_component_validation_failures = true`, `allow_unconnected_components = true`, plus the pre-0.5 formula generators (which behave like `prefer_meters_in_component_formulas = false` and `include_phantom_loads_in_consumer_formula = true`). To preserve the old behavior, pass a `ComponentGraphConfig` built with those four flags via `LogicalMeterConfig::with_component_graph_config`.

- `MicrogridClientHandle::try_new`, `LogicalMeterHandle::try_new`, and `Microgrid::try_new` no longer return an error when the microgrid API server is unreachable at startup or when the server returns data that doesn't yet form a valid component graph; instead they wait for the server to recover. Callers that relied on a quick failure to detect a misconfigured or unavailable endpoint should wrap the call in `tokio::time::timeout` (or equivalent) to bound the wait. URL validation still fails fast: a malformed endpoint URL is still surfaced as `ConnectionFailure` from `MicrogridClientHandle::try_new`, and an invalid `LogicalMeterConfig` still surfaces synchronously from `LogicalMeterHandle::try_new`.

Expand All @@ -13,10 +13,11 @@
- The microgrid client now tolerates the API server being absent or returning incomplete data at startup. `MicrogridClientHandle::try_new` establishes the gRPC connection lazily, so it succeeds regardless of whether the server is reachable; transient stream errors are then handled by the existing per-stream retry loop. `LogicalMeterHandle::try_new` (and therefore `Microgrid::try_new`) wraps the entire component-graph setup — listing components, listing connections, and building the graph — in a single retry loop that sleeps 3 seconds between attempts, so applications block waiting for the server and a valid graph instead of exiting with an error.

- `Bounds::combine_parallel`, `Bounds::intersect`, and `Bounds::merge_if_overlapping` are now public, allowing external callers to combine bounds without going through higher-level types.
- Put test utils under a feature gate.

- Expose test utils behind the `test-utils` feature gate.

- Added `MockMicrogridApiClient::augment_electrical_component_bounds`: It captures requests so that these can be used in test cases. Obtain the list of captured requests using `MockMicrogridApiClient::augment_bounds_calls_handle` (also new).
- Added `MockComponent.add_component_bounds`: It allows to add metric bounds to a mock component.

## Bug Fixes
- Added `MockComponent.add_component_bounds`: It allows to add metric bounds to a mock component.

<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
- `LogicalMeterConfig::with_component_graph_config` lets callers pass a custom `ComponentGraphConfig` to the underlying graph builder (e.g. to enable phantom loads in the consumer formula or to flip the meter-vs-device preference for per-category formulas).
4 changes: 2 additions & 2 deletions src/client/microgrid_client_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ impl MicrogridClientHandle {
///
/// The direction of a connection is always away from the grid endpoint,
/// i.e. aligned with the direction of positive current according to the
/// passive sign convention:
/// https://en.wikipedia.org/wiki/Passive_sign_convention
/// [passive sign
/// convention](https://en.wikipedia.org/wiki/Passive_sign_convention).
///
/// If provided, the `start` and `end` filters have an `AND` relationship
/// between each other, meaning that they are applied serially, but an `OR`
Expand Down
3 changes: 1 addition & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ ErrorKind!(
(APIServerError, api_server_error),
);

/// An error that can occur during the creation or traversal of a
/// [ComponentGraph][crate::ComponentGraph].
/// An error that occurred in `frequenz_microgrid`.
#[derive(Debug, Clone, PartialEq)]
pub struct Error {
kind: ErrorKind,
Expand Down
3 changes: 1 addition & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! High-level interface for the Microgrid API.

#![doc = include_str!("../README.md")]
#![cfg_attr(
not(test),
deny(
Expand Down
19 changes: 19 additions & 0 deletions src/logical_meter/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use crate::Sample;
use crate::client::proto::common::metrics::Metric;
use chrono::TimeDelta;
use frequenz_microgrid_component_graph::ComponentGraphConfig;
use frequenz_resampling::ResamplingFunction;
use std::collections::HashMap;

Expand All @@ -19,6 +20,11 @@ pub struct LogicalMeterConfig {
/// The maximum age of samples to be considered for resampling, in number of
/// intervals.
pub(crate) max_age_in_intervals: u32,
/// Configuration forwarded to the underlying [`ComponentGraph`][cg]. Defaults
/// to [`ComponentGraphConfig::default()`].
///
/// [cg]: frequenz_microgrid_component_graph::ComponentGraph
pub(crate) component_graph_config: ComponentGraphConfig,
}

impl LogicalMeterConfig {
Expand All @@ -29,6 +35,7 @@ impl LogicalMeterConfig {
resampling_function: None,
resampling_overrides: HashMap::new(),
max_age_in_intervals: 3,
component_graph_config: ComponentGraphConfig::default(),
}
}

Expand Down Expand Up @@ -72,4 +79,16 @@ impl LogicalMeterConfig {
self.max_age_in_intervals = max_age_in_intervals.max(1);
self
}

/// Sets the [`ComponentGraphConfig`] forwarded to the underlying graph
/// when [`LogicalMeterHandle::try_new`][lm] (and therefore
/// [`Microgrid::try_new`][mg]) builds it. If not set, the graph crate's
/// `Default::default()` is used.
///
/// [lm]: crate::LogicalMeterHandle::try_new
/// [mg]: crate::Microgrid::try_new
pub fn with_component_graph_config(mut self, config: ComponentGraphConfig) -> Self {
self.component_graph_config = config;
self
}
}
46 changes: 28 additions & 18 deletions src/logical_meter/logical_meter_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
error::Error,
metric,
};
use frequenz_microgrid_component_graph::{self, ComponentGraph};
use frequenz_microgrid_component_graph::{self, ComponentGraph, ComponentGraphConfig};
use std::collections::BTreeSet;
use std::time::Duration;
use tokio::sync::mpsc;
Expand Down Expand Up @@ -47,7 +47,7 @@ impl LogicalMeterHandle {
let (sender, receiver) = mpsc::channel(8);
const RETRY_DELAY: Duration = Duration::from_secs(3);
let graph = loop {
match build_component_graph(&client).await {
match build_component_graph(&client, &config.component_graph_config).await {
Ok(g) => break g,
Err(reason) => {
tracing::warn!(
Expand Down Expand Up @@ -183,6 +183,7 @@ impl LogicalMeterHandle {
/// the retry loop can log a concise reason.
async fn build_component_graph(
client: &MicrogridClientHandle,
config: &ComponentGraphConfig,
) -> Result<ComponentGraph<ElectricalComponent, ElectricalComponentConnection>, String> {
let components = client
.list_electrical_components(vec![], vec![])
Expand All @@ -192,17 +193,8 @@ async fn build_component_graph(
.list_electrical_component_connections(vec![], vec![])
.await
.map_err(|e| format!("fetching component connections failed: {e}"))?;
ComponentGraph::try_new(
components,
connections,
frequenz_microgrid_component_graph::ComponentGraphConfig {
allow_component_validation_failures: true,
allow_unconnected_components: true,
allow_unspecified_inverters: false,
disable_fallback_components: false,
},
)
.map_err(|e| format!("building component graph failed: {e}"))
ComponentGraph::try_new(components, connections, config.clone())
.map_err(|e| format!("building component graph failed: {e}"))
}

#[cfg(test)]
Expand All @@ -211,6 +203,8 @@ mod tests {
use frequenz_resampling::ResamplingFunction;
use tokio_stream::{StreamExt, wrappers::BroadcastStream};

use frequenz_microgrid_component_graph::ComponentGraphConfig;

use crate::{
LogicalMeterConfig, LogicalMeterHandle, MicrogridClientHandle, Sample,
client::test_utils::{
Expand Down Expand Up @@ -284,7 +278,16 @@ mod tests {

#[tokio::test]
async fn test_formula_display() {
let lm = new_logical_meter_handle(None).await;
let lm = new_logical_meter_handle(Some(
LogicalMeterConfig::new(TimeDelta::try_seconds(1).unwrap())
.with_component_graph_config(
ComponentGraphConfig::builder()
.prefer_meters_in_component_formulas(false)
.include_phantom_loads_in_consumer_formula(true)
.build(),
),
))
.await;

let formula = lm.grid::<crate::metric::AcPowerActive>().unwrap();
assert_eq!(formula.to_string(), "METRIC_AC_POWER_ACTIVE::(#2)");
Expand Down Expand Up @@ -554,10 +557,17 @@ mod tests {

#[tokio::test(start_paused = true)]
async fn test_consumer_current_formula() {
let formula = new_logical_meter_handle(None)
.await
.consumer::<crate::metric::AcCurrent>()
.unwrap();
let formula = new_logical_meter_handle(Some(
LogicalMeterConfig::new(TimeDelta::try_seconds(1).unwrap())
.with_component_graph_config(
ComponentGraphConfig::builder()
.include_phantom_loads_in_consumer_formula(true)
.build(),
),
))
.await
.consumer::<crate::metric::AcCurrent>()
.unwrap();

let samples = fetch_samples(formula, 10).await;
check_samples(
Expand Down
Loading