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 @@ [docs.rs](https://docs.rs/frequenz-microgrid) [Crates.io](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(