diff --git a/Cargo.toml b/Cargo.toml
index c516d98..2223a24 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
@@ -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"
diff --git a/README.md b/README.md
index 0ac6d8d..bde066f 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,125 @@
[
](https://docs.rs/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.
+
+## 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::()?
+ .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
+[`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
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index b678fe0..5fae162 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,10 +1,10 @@
# Frequenz Microgrid Release Notes
-## Summary
+## Upgrading
-
+- 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`.
@@ -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.
-
+- `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).
diff --git a/src/client/microgrid_client_handle.rs b/src/client/microgrid_client_handle.rs
index c785c50..b3721ef 100644
--- a/src/client/microgrid_client_handle.rs
+++ b/src/client/microgrid_client_handle.rs
@@ -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`
diff --git a/src/error.rs b/src/error.rs
index 30848f1..3bcbc10 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -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,
diff --git a/src/lib.rs b/src/lib.rs
index 565eab9..a5eda7c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(
diff --git a/src/logical_meter/config.rs b/src/logical_meter/config.rs
index b3ada64..fb51cc9 100644
--- a/src/logical_meter/config.rs
+++ b/src/logical_meter/config.rs
@@ -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;
@@ -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 {
@@ -29,6 +35,7 @@ impl LogicalMeterConfig {
resampling_function: None,
resampling_overrides: HashMap::new(),
max_age_in_intervals: 3,
+ component_graph_config: ComponentGraphConfig::default(),
}
}
@@ -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
+ }
}
diff --git a/src/logical_meter/logical_meter_handle.rs b/src/logical_meter/logical_meter_handle.rs
index 469d24b..139c096 100644
--- a/src/logical_meter/logical_meter_handle.rs
+++ b/src/logical_meter/logical_meter_handle.rs
@@ -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;
@@ -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!(
@@ -183,6 +183,7 @@ impl LogicalMeterHandle {
/// the retry loop can log a concise reason.
async fn build_component_graph(
client: &MicrogridClientHandle,
+ config: &ComponentGraphConfig,
) -> Result, String> {
let components = client
.list_electrical_components(vec![], vec![])
@@ -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)]
@@ -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::{
@@ -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::().unwrap();
assert_eq!(formula.to_string(), "METRIC_AC_POWER_ACTIVE::(#2)");
@@ -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::()
- .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::()
+ .unwrap();
let samples = fetch_samples(formula, 10).await;
check_samples(