From 90b0c760737a728301e77d334920e060161d1408 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 16 Mar 2026 15:54:53 +0100 Subject: [PATCH 01/10] Expect `ElectricalComponentCategory` enum instead of `i32` in client This makes the high-level interface strongly typed. Signed-off-by: Sahas Subramanian --- src/client/instruction.rs | 5 +-- src/client/microgrid_client_actor.rs | 5 ++- src/client/microgrid_client_handle.rs | 48 +++++++++++++++++++++++++-- src/client/test_utils.rs | 10 ++++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/client/instruction.rs b/src/client/instruction.rs index 14bab82..adcc074 100644 --- a/src/client/instruction.rs +++ b/src/client/instruction.rs @@ -8,7 +8,8 @@ use tokio::sync::{broadcast, oneshot}; use crate::{ Error, proto::common::microgrid::electrical_components::{ - ElectricalComponent, ElectricalComponentConnection, ElectricalComponentTelemetry, + ElectricalComponent, ElectricalComponentCategory, ElectricalComponentConnection, + ElectricalComponentTelemetry, }, }; @@ -21,7 +22,7 @@ pub(super) enum Instruction { }, ListElectricalComponents { electrical_component_ids: Vec, - electrical_component_categories: Vec, + electrical_component_categories: Vec, response_tx: oneshot::Sender, Error>>, }, ListElectricalComponentConnections { diff --git a/src/client/microgrid_client_actor.rs b/src/client/microgrid_client_actor.rs index 30a8fd8..9ea549f 100644 --- a/src/client/microgrid_client_actor.rs +++ b/src/client/microgrid_client_actor.rs @@ -149,7 +149,10 @@ async fn handle_instruction( let components = client .list_electrical_components(ListElectricalComponentsRequest { electrical_component_ids, - electrical_component_categories, + electrical_component_categories: electrical_component_categories + .into_iter() + .map(|c| c as i32) + .collect(), }) .await .map_err(|e| Error::connection_failure(format!("list_components failed: {e}"))) diff --git a/src/client/microgrid_client_handle.rs b/src/client/microgrid_client_handle.rs index 78a2114..fec6cfa 100644 --- a/src/client/microgrid_client_handle.rs +++ b/src/client/microgrid_client_handle.rs @@ -14,7 +14,8 @@ use crate::{ client::MicrogridApiClient, proto::{ common::microgrid::electrical_components::{ - ElectricalComponent, ElectricalComponentConnection, ElectricalComponentTelemetry, + ElectricalComponent, ElectricalComponentCategory, ElectricalComponentConnection, + ElectricalComponentTelemetry, }, microgrid::microgrid_client::MicrogridClient, }, @@ -99,7 +100,7 @@ impl MicrogridClientHandle { pub async fn list_electrical_components( &self, electrical_component_ids: Vec, - electrical_component_categories: Vec, + electrical_component_categories: Vec, ) -> Result, Error> { let (response_tx, response_rx) = oneshot::channel(); @@ -164,7 +165,10 @@ mod tests { use crate::{ MicrogridClientHandle, client::test_utils::{MockComponent, MockMicrogridApiClient}, - proto::common::metrics::{SimpleMetricValue, metric_value_variant}, + proto::common::{ + metrics::{SimpleMetricValue, metric_value_variant}, + microgrid::electrical_components::ElectricalComponentCategory, + }, }; fn new_client_handle() -> MicrogridClientHandle { @@ -207,6 +211,44 @@ mod tests { assert_eq!(component_ids, vec![1, 2, 3, 4, 5, 6, 7]); } + #[tokio::test] + async fn test_list_electrical_components_with_filters() { + let handle = new_client_handle(); + + let components = handle + .list_electrical_components(vec![1, 2], vec![]) + .await + .unwrap(); + let component_ids: Vec = components.iter().map(|c| c.id).collect(); + assert_eq!(component_ids, vec![1, 2]); + + let components = handle + .list_electrical_components( + vec![], + vec![ + ElectricalComponentCategory::Meter, + ElectricalComponentCategory::Battery, + ], + ) + .await + .unwrap(); + let component_ids: Vec = components.iter().map(|c| c.id).collect(); + assert_eq!(component_ids, vec![2, 3, 5, 7]); + + let components = handle + .list_electrical_components( + vec![2, 3, 4], + vec![ + ElectricalComponentCategory::Meter, + ElectricalComponentCategory::Battery, + ], + ) + .await + .unwrap(); + let component_ids: Vec = components.iter().map(|c| c.id).collect(); + assert_eq!(component_ids, vec![2, 3]); + } + #[tokio::test] async fn test_list_electrical_component_connections() { let handle = new_client_handle(); diff --git a/src/client/test_utils.rs b/src/client/test_utils.rs index dcf6cb4..2defc3e 100644 --- a/src/client/test_utils.rs +++ b/src/client/test_utils.rs @@ -280,10 +280,20 @@ impl MicrogridApiClient for MockMicrogridApiClient { &mut self, _request: impl tonic::IntoRequest + Send, ) -> std::result::Result, tonic::Status> { + let ListElectricalComponentsRequest { + electrical_component_ids, + electrical_component_categories, + } = _request.into_request().into_inner(); Ok(Response::new(ListElectricalComponentsResponse { electrical_components: self .components .iter() + .filter(|c| { + (electrical_component_ids.is_empty() + || electrical_component_ids.contains(&c.component.id)) + && (electrical_component_categories.is_empty() + || electrical_component_categories.contains(&c.component.category)) + }) .map(|c| c.component.clone()) .collect(), })) From 27bf2fee4ae44faa1e2a23f8431bdc229449de92 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 17 Mar 2026 10:53:39 +0100 Subject: [PATCH 02/10] Promote metric module to top-level public module This makes it easy to reuse it in the client. Signed-off-by: Sahas Subramanian --- src/lib.rs | 6 +++--- src/logical_meter.rs | 3 +-- src/logical_meter/logical_meter_handle.rs | 17 +++++++++-------- src/{logical_meter => }/metric.rs | 5 ++--- 4 files changed, 15 insertions(+), 16 deletions(-) rename src/{logical_meter => }/metric.rs (95%) diff --git a/src/lib.rs b/src/lib.rs index cfda580..f19c25f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,6 @@ mod sample; pub use sample::Sample; mod logical_meter; -pub use logical_meter::{ - Formula, FormulaSubscriber, LogicalMeterConfig, LogicalMeterHandle, metric, -}; +pub use logical_meter::{Formula, FormulaSubscriber, LogicalMeterConfig, LogicalMeterHandle}; + +pub mod metric; diff --git a/src/logical_meter.rs b/src/logical_meter.rs index 25b8b6e..620bee8 100644 --- a/src/logical_meter.rs +++ b/src/logical_meter.rs @@ -4,12 +4,11 @@ //! Logical meter implementation for the Frequenz microgrid API. mod config; -mod formula; +pub(crate) mod formula; pub use formula::{Formula, FormulaSubscriber}; mod logical_meter_actor; mod logical_meter_handle; pub use logical_meter_handle::LogicalMeterHandle; -pub mod metric; pub use config::LogicalMeterConfig; diff --git a/src/logical_meter/logical_meter_handle.rs b/src/logical_meter/logical_meter_handle.rs index f4cb102..e55a3de 100644 --- a/src/logical_meter/logical_meter_handle.rs +++ b/src/logical_meter/logical_meter_handle.rs @@ -6,6 +6,7 @@ use crate::logical_meter::formula::graph_formula_provider::GraphFormulaProvider; use crate::{ client::MicrogridClientHandle, error::Error, + metric, proto::common::microgrid::electrical_components::{ ElectricalComponent, ElectricalComponentConnection, }, @@ -60,7 +61,7 @@ impl LogicalMeterHandle { /// Returns a receiver that streams samples for the given `metric` at the grid /// connection point. - pub fn grid( + pub fn grid( &mut self, metric: M, ) -> Result, Error> { @@ -75,7 +76,7 @@ impl LogicalMeterHandle { /// given battery IDs. /// /// When `component_ids` is `None`, all batteries in the microgrid are used. - pub fn battery( + pub fn battery( &mut self, component_ids: Option>, metric: M, @@ -92,7 +93,7 @@ impl LogicalMeterHandle { /// given CHP IDs. /// /// When `component_ids` is `None`, all CHPs in the microgrid are used. - pub fn chp( + pub fn chp( &mut self, component_ids: Option>, metric: M, @@ -109,7 +110,7 @@ impl LogicalMeterHandle { /// given PV IDs. /// /// When `component_ids` is `None`, all PVs in the microgrid are used. - pub fn pv( + pub fn pv( &mut self, component_ids: Option>, metric: M, @@ -127,7 +128,7 @@ impl LogicalMeterHandle { /// /// When `component_ids` is `None`, all EV chargers in the microgrid are /// used. - pub fn ev_charger( + pub fn ev_charger( &mut self, component_ids: Option>, metric: M, @@ -142,7 +143,7 @@ impl LogicalMeterHandle { /// Returns a receiver that streams samples for the given `metric` for the /// logical `consumer` in the microgrid. - pub fn consumer( + pub fn consumer( &mut self, metric: M, ) -> Result, Error> { @@ -155,7 +156,7 @@ impl LogicalMeterHandle { /// Returns a receiver that streams samples for the given `metric` for the /// logical `producer` in the microgrid. - pub fn producer( + pub fn producer( &mut self, metric: M, ) -> Result, Error> { @@ -168,7 +169,7 @@ impl LogicalMeterHandle { /// Returns a receiver that streams samples for the given `metric` for the /// given component ID. - pub fn component( + pub fn component( &mut self, component_id: u64, metric: M, diff --git a/src/logical_meter/metric.rs b/src/metric.rs similarity index 95% rename from src/logical_meter/metric.rs rename to src/metric.rs index bd00f51..0ea50d6 100644 --- a/src/logical_meter/metric.rs +++ b/src/metric.rs @@ -6,11 +6,10 @@ use crate::logical_meter::formula::aggregation_formula::AggregationFormula; use crate::logical_meter::formula::coalesce_formula::CoalesceFormula; use crate::{ - logical_meter::formula::FormulaSubscriber, proto::common::metrics::Metric as MetricPb, + logical_meter::formula, logical_meter::formula::FormulaSubscriber, + proto::common::metrics::Metric as MetricPb, }; -use super::formula; - pub trait Metric: std::fmt::Display + std::fmt::Debug + Clone + Copy + PartialEq + Eq + Sync + 'static { From 65708781b3487facb0f152a20327a56f24bfbba6 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 20 Mar 2026 10:08:08 +0100 Subject: [PATCH 03/10] Make the Quantity methods `const` Signed-off-by: Sahas Subramanian --- src/quantity.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/quantity.rs b/src/quantity.rs index d68ca60..bcd3804 100644 --- a/src/quantity.rs +++ b/src/quantity.rs @@ -103,10 +103,10 @@ macro_rules! qty_format { macro_rules! qty_ctor { (@impl ($ctor:ident, $getter:ident, $unit:tt, $exp:literal) $(,)?) => { - pub fn $ctor(value: f32) -> Self { + pub const fn $ctor(value: f32) -> Self { Self { value: value * $exp } } - pub fn $getter(&self) -> f32 { + pub const fn $getter(&self) -> f32 { self.value / $exp } }; @@ -190,47 +190,47 @@ macro_rules! qty_ctor { impl $typename { qty_ctor!(@impl $($rest)*); - pub fn abs(&self) -> Self { + pub const fn abs(&self) -> Self { Self { value: self.value.abs(), } } - pub fn floor(&self) -> Self { + pub const fn floor(&self) -> Self { Self { value: self.value.floor(), } } - pub fn ceil(&self) -> Self { + pub const fn ceil(&self) -> Self { Self { value: self.value.ceil(), } } - pub fn round(&self) -> Self { + pub const fn round(&self) -> Self { Self { value: self.value.round(), } } - pub fn trunc(&self) -> Self { + pub const fn trunc(&self) -> Self { Self { value: self.value.trunc(), } } - pub fn fract(&self) -> Self { + pub const fn fract(&self) -> Self { Self { value: self.value.fract(), } } - pub fn is_nan(&self) -> bool { + pub const fn is_nan(&self) -> bool { self.value.is_nan() } - pub fn is_infinite(&self) -> bool { + pub const fn is_infinite(&self) -> bool { self.value.is_infinite() } } From 63deb53a86ed1c9f534301495c9655242981eee2 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 20 Mar 2026 10:08:56 +0100 Subject: [PATCH 04/10] Add `min` and `max` methods on `Quantity` types Signed-off-by: Sahas Subramanian --- src/quantity.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/quantity.rs b/src/quantity.rs index bcd3804..1a46527 100644 --- a/src/quantity.rs +++ b/src/quantity.rs @@ -233,6 +233,18 @@ macro_rules! qty_ctor { pub const fn is_infinite(&self) -> bool { self.value.is_infinite() } + + pub const fn min(self, other: Self) -> Self { + Self { + value: self.value.min(other.value), + } + } + + pub const fn max(self, other: Self) -> Self { + Self { + value: self.value.max(other.value), + } + } } qty_ctor!{@impl_arith_ops $typename} From 773fe542d8d0ceb71c4c9c3799ce53ce7d39db8c Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 20 Mar 2026 10:20:01 +0100 Subject: [PATCH 05/10] Add a `Bounds` type generic over quantities Signed-off-by: Sahas Subramanian --- src/bounds.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +++ 2 files changed, 70 insertions(+) create mode 100644 src/bounds.rs diff --git a/src/bounds.rs b/src/bounds.rs new file mode 100644 index 0000000..80aff8f --- /dev/null +++ b/src/bounds.rs @@ -0,0 +1,67 @@ +// License: MIT +// Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +//! A representation of Bounds for any metric. + +use crate::proto::common::metrics::Bounds as PbBounds; +use crate::quantity::{Current, Power, Quantity, ReactivePower}; + +/// A set of lower and upper bounds for any metric. +pub struct Bounds { + /// The lower bound. + /// If None, there is no lower bound. + lower: Option, + /// The upper bound. + /// If None, there is no upper bound. + upper: Option, +} + +impl Bounds { + /// Creates a new `Bounds` with the given lower and upper bounds. + pub fn new(lower: Option, upper: Option) -> Self { + Self { lower, upper } + } + + /// Returns the lower bound. + pub fn lower(&self) -> Option { + self.lower + } + + /// Returns the upper bound. + pub fn upper(&self) -> Option { + self.upper + } +} + +impl From<(Option, Option)> for Bounds { + fn from(bounds: (Option, Option)) -> Self { + Self::new(bounds.0, bounds.1) + } +} + +impl From> for PbBounds { + fn from(bounds: Bounds) -> Self { + PbBounds { + lower: bounds.lower.map(|q| q.as_watts()), + upper: bounds.upper.map(|q| q.as_watts()), + } + } +} + +impl From> for PbBounds { + fn from(bounds: Bounds) -> Self { + PbBounds { + lower: bounds.lower.map(|q| q.as_amperes()), + upper: bounds.upper.map(|q| q.as_amperes()), + } + } +} + +impl From> for PbBounds { + fn from(bounds: Bounds) -> Self { + PbBounds { + lower: bounds.lower.map(|q| q.as_volt_amperes_reactive()), + upper: bounds.upper.map(|q| q.as_volt_amperes_reactive()), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f19c25f..8399187 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,9 @@ //! High-level interface for the Microgrid API. +mod bounds; +pub use bounds::Bounds; + mod client; pub use client::MicrogridClientHandle; From 35b40a3e8cdd50be181d400cb26553b7285b6452 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 20 Mar 2026 10:33:30 +0100 Subject: [PATCH 06/10] Support augmenting bounds from the client Signed-off-by: Sahas Subramanian --- src/client/instruction.rs | 17 +++++++-- src/client/microgrid_api_client.rs | 14 +++++++ src/client/microgrid_client_actor.rs | 37 +++++++++++++++++- src/client/microgrid_client_handle.rs | 54 ++++++++++++++++++++++++++- src/error.rs | 3 +- 5 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/client/instruction.rs b/src/client/instruction.rs index adcc074..62b8770 100644 --- a/src/client/instruction.rs +++ b/src/client/instruction.rs @@ -3,13 +3,17 @@ //! Instructions that can be sent to the client actor from client handles. +use chrono::TimeDelta; use tokio::sync::{broadcast, oneshot}; use crate::{ Error, - proto::common::microgrid::electrical_components::{ - ElectricalComponent, ElectricalComponentCategory, ElectricalComponentConnection, - ElectricalComponentTelemetry, + proto::common::{ + metrics::{Bounds, Metric}, + microgrid::electrical_components::{ + ElectricalComponent, ElectricalComponentCategory, ElectricalComponentConnection, + ElectricalComponentTelemetry, + }, }, }; @@ -30,4 +34,11 @@ pub(super) enum Instruction { destination_electrical_component_ids: Vec, response_tx: oneshot::Sender, Error>>, }, + AugmentElectricalComponentBounds { + electrical_component_id: u64, + target_metric: Metric, + bounds: Vec, + request_lifetime: Option, + response_tx: oneshot::Sender>, Error>>, + }, } diff --git a/src/client/microgrid_api_client.rs b/src/client/microgrid_api_client.rs index d206583..8786c49 100644 --- a/src/client/microgrid_api_client.rs +++ b/src/client/microgrid_api_client.rs @@ -7,6 +7,7 @@ use tonic::transport::Channel; use crate::proto::microgrid::{ + AugmentElectricalComponentBoundsRequest, AugmentElectricalComponentBoundsResponse, ListElectricalComponentConnectionsRequest, ListElectricalComponentConnectionsResponse, ListElectricalComponentsRequest, ListElectricalComponentsResponse, ReceiveElectricalComponentTelemetryStreamRequest, @@ -44,6 +45,11 @@ pub trait MicrogridApiClient: Send + Sync + 'static { &mut self, request: impl tonic::IntoRequest + Send, ) -> std::result::Result, tonic::Status>; + + async fn augment_electrical_component_bounds( + &mut self, + request: impl tonic::IntoRequest + Send, + ) -> std::result::Result, tonic::Status>; } /// Implement the MicrogridApiClient trait for the generated gRPC client. @@ -78,4 +84,12 @@ impl MicrogridApiClient for crate::proto::microgrid::microgrid_client::Microgrid self.receive_electrical_component_telemetry_stream(request) .await } + + async fn augment_electrical_component_bounds( + &mut self, + request: impl tonic::IntoRequest + Send, + ) -> std::result::Result, tonic::Status> + { + self.augment_electrical_component_bounds(request).await + } } diff --git a/src/client/microgrid_client_actor.rs b/src/client/microgrid_client_actor.rs index 9ea549f..8fb926e 100644 --- a/src/client/microgrid_client_actor.rs +++ b/src/client/microgrid_client_actor.rs @@ -11,9 +11,9 @@ use crate::{ ReceiveElectricalComponentTelemetryStreamResponse, }, }; -use std::collections::HashMap; - +use chrono::DateTime; use futures::{Stream, StreamExt}; +use std::collections::HashMap; use tokio::{ select, sync::{broadcast, mpsc}, @@ -180,6 +180,39 @@ async fn handle_instruction( .send(connections) .map_err(|_| Error::internal("failed to send response"))?; } + Some(Instruction::AugmentElectricalComponentBounds { + electrical_component_id, + target_metric, + bounds, + request_lifetime, + response_tx, + }) => { + let response = client + .augment_electrical_component_bounds( + crate::proto::microgrid::AugmentElectricalComponentBoundsRequest { + electrical_component_id, + target_metric: target_metric as i32, + bounds: bounds, + request_lifetime: request_lifetime.map(|d| d.as_seconds_f64() as u64), + }, + ) + .await + .map_err(|e| { + Error::api_server_error(format!( + "augment_electrical_component_bounds failed: {e}" + )) + }) + .map(|r| { + r.into_inner() + .valid_until_time + .map(|t| DateTime::from_timestamp(t.seconds, t.nanos as u32)) + .flatten() + }); + + response_tx + .send(response) + .map_err(|_| Error::internal("failed to send response"))?; + } None => {} } diff --git a/src/client/microgrid_client_handle.rs b/src/client/microgrid_client_handle.rs index fec6cfa..e09014b 100644 --- a/src/client/microgrid_client_handle.rs +++ b/src/client/microgrid_client_handle.rs @@ -6,13 +6,16 @@ //! Instructions received by this handle are sent to the microgrid client actor, //! which owns the connection to the microgrid API service. +use chrono::TimeDelta; use tokio::sync::{broadcast, mpsc, oneshot}; use tonic::transport::Channel; use crate::{ - Error, + Bounds, Error, client::MicrogridApiClient, + metric::Metric, proto::{ + common::metrics::Bounds as PbBounds, common::microgrid::electrical_components::{ ElectricalComponent, ElectricalComponentCategory, ElectricalComponentConnection, ElectricalComponentTelemetry, @@ -155,6 +158,55 @@ impl MicrogridClientHandle { .await .map_err(|e| Error::internal(format!("failed to receive response: {e}")))? } + + /// Augments the overall bounds for a given metric of a given electrical + /// component with the provided bounds. + /// Returns the UTC time at which the provided bounds will expire and its + /// effects will no longer be visible in the overall bounds for the + /// given metric. + /// + /// The request parameters allows users to select a duration until + /// which the bounds will stay in effect. If no duration is provided, then the + /// bounds will be removed after a default duration of 5 seconds. + /// + /// Inclusion bounds give the range that the system will try to keep the + /// metric within. If the metric goes outside of these bounds, the system will + /// try to bring it back within the bounds. + /// If the bounds for a metric are [Symbol’s value as variable is void: lower_1End. + /// ---- values here are considered out of range. + /// ==== values here are considered within range. + /// + /// Note that for power metrics, regardless of the bounds, 0W is always + /// allowed. + pub async fn augment_electrical_component_bounds( + &mut self, + electrical_component_id: u64, + #[allow(unused_variables)] target_metric: M, + bounds: Vec, + request_lifetime: Option, + ) -> Result>, Error> + where + M: Metric, + Bounds: Into, + I: Into>, + { + let (response_tx, response_rx) = oneshot::channel(); + + self.instructions_tx + .send(Instruction::AugmentElectricalComponentBounds { + response_tx, + electrical_component_id, + target_metric: M::METRIC, + bounds: bounds.into_iter().map(|x| x.into().into()).collect(), + request_lifetime, + }) + .await + .map_err(|_| Error::internal("failed to send instruction"))?; + + response_rx + .await + .map_err(|e| Error::internal(format!("failed to receive response: {e}")))? + } } #[cfg(test)] diff --git a/src/error.rs b/src/error.rs index 7e78681..5794900 100644 --- a/src/error.rs +++ b/src/error.rs @@ -58,7 +58,8 @@ ErrorKind!( (ChronoError, chrono_error), (DroppedUnusedFormulas, dropped_unused_formulas), (FormulaEngineError, formula_engine_error), - (Internal, internal) + (Internal, internal), + (APIServerError, api_server_error), ); /// An error that can occur during the creation or traversal of a From afda6750346850b8bbd714e81261cfb3fdc3d6f9 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 20 Mar 2026 11:08:33 +0100 Subject: [PATCH 07/10] Move `proto` to be a sub-module of `client` This makes it possible to expose proto-types that are only needed for accessing `client` methods, to be exposed from the `client`. Later on, there should be higher level interfaces for all these functions, so using the proto types directly wouldn't be necessary. Signed-off-by: Sahas Subramanian --- src/client.rs | 4 +++- src/{ => client}/proto.rs | 0 src/{ => client}/proto/graph.rs | 0 src/lib.rs | 5 ++--- 4 files changed, 5 insertions(+), 4 deletions(-) rename src/{ => client}/proto.rs (100%) rename src/{ => client}/proto/graph.rs (100%) diff --git a/src/client.rs b/src/client.rs index 0bd1861..4e9df7f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,10 +8,12 @@ mod microgrid_client_actor; mod retry_tracker; mod microgrid_api_client; -pub use microgrid_api_client::MicrogridApiClient; +pub(crate) use microgrid_api_client::MicrogridApiClient; mod microgrid_client_handle; pub use microgrid_client_handle::MicrogridClientHandle; +pub(crate) mod proto; + #[cfg(test)] pub(crate) mod test_utils; diff --git a/src/proto.rs b/src/client/proto.rs similarity index 100% rename from src/proto.rs rename to src/client/proto.rs diff --git a/src/proto/graph.rs b/src/client/proto/graph.rs similarity index 100% rename from src/proto/graph.rs rename to src/client/proto/graph.rs diff --git a/src/lib.rs b/src/lib.rs index 8399187..f6b36ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,16 +6,15 @@ mod bounds; pub use bounds::Bounds; -mod client; +pub mod client; pub use client::MicrogridClientHandle; +pub(crate) use client::proto; mod error; pub use error::{Error, ErrorKind}; pub mod quantity; -mod proto; - mod sample; pub use sample::Sample; From d847c381b0aadf652a7cbde74a00d4c802b6c5d3 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 20 Mar 2026 11:10:30 +0100 Subject: [PATCH 08/10] Expose `ElectricalComponentCategory` from the client Signed-off-by: Sahas Subramanian --- src/client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client.rs b/src/client.rs index 4e9df7f..c6379b4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,6 +14,7 @@ mod microgrid_client_handle; pub use microgrid_client_handle::MicrogridClientHandle; pub(crate) mod proto; +pub use proto::common::microgrid::electrical_components::ElectricalComponentCategory; #[cfg(test)] pub(crate) mod test_utils; From 6a0663739d74a88e333cbf0815eb735d7e7bbcb5 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 20 Mar 2026 14:06:33 +0100 Subject: [PATCH 09/10] Implement Display for `Sample` Signed-off-by: Sahas Subramanian --- src/sample.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/sample.rs b/src/sample.rs index 22c1e4b..19b2098 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -5,12 +5,26 @@ use chrono::{DateTime, Utc}; /// Represents a measurement of a microgrid metric, made at a specific time. #[derive(Copy, Clone, Debug, Default)] -pub struct Sample { +pub struct Sample { pub(crate) timestamp: DateTime, pub(crate) value: Option, } -impl frequenz_resampling::Sample for Sample { +impl std::fmt::Display for Sample { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Sample({}, ", self.timestamp)?; + + if let Some(value) = self.value { + write!(f, " {})", value) + } else { + write!(f, " None)") + } + } +} + +impl frequenz_resampling::Sample + for Sample +{ type Value = Q; fn new(timestamp: DateTime, value: Option) -> Self { @@ -26,7 +40,7 @@ impl frequenz_resampling::Sample fo } } -impl Sample { +impl Sample { /// Creates a new `Sample` with the given timestamp and value. pub fn new(timestamp: DateTime, value: Option) -> Self { Self { timestamp, value } From abbfdff8f9b7a6436ed8333a06ea437beefcaab7 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 20 Mar 2026 14:09:13 +0100 Subject: [PATCH 10/10] Add methods to identify the type of an inverter Signed-off-by: Sahas Subramanian --- src/client.rs | 4 +- src/client/proto.rs | 1 + src/client/proto/electrical_component.rs | 60 ++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/client/proto/electrical_component.rs diff --git a/src/client.rs b/src/client.rs index c6379b4..9a33a00 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,7 +14,9 @@ mod microgrid_client_handle; pub use microgrid_client_handle::MicrogridClientHandle; pub(crate) mod proto; -pub use proto::common::microgrid::electrical_components::ElectricalComponentCategory; +pub use proto::common::microgrid::electrical_components::{ + ElectricalComponent, ElectricalComponentCategory, +}; #[cfg(test)] pub(crate) mod test_utils; diff --git a/src/client/proto.rs b/src/client/proto.rs index d33a729..fab25a5 100644 --- a/src/client/proto.rs +++ b/src/client/proto.rs @@ -20,4 +20,5 @@ pub use pb::frequenz::api::microgrid::v1alpha18 as microgrid; #[cfg(test)] pub use pb::google; +mod electrical_component; mod graph; diff --git a/src/client/proto/electrical_component.rs b/src/client/proto/electrical_component.rs new file mode 100644 index 0000000..5c5e56e --- /dev/null +++ b/src/client/proto/electrical_component.rs @@ -0,0 +1,60 @@ +// License: MIT +// Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +//! Extensions to the generated protobuf code for electrical components. + +use crate::{ + client::{ElectricalComponent, ElectricalComponentCategory}, + proto::common::microgrid::electrical_components::{ + InverterType, electrical_component_category_specific_info::Kind, + }, +}; + +impl ElectricalComponent { + /// Returns true if the component is an inverter, false otherwise. + pub fn is_inverter(&self) -> bool { + matches!( + ElectricalComponentCategory::try_from(self.category), + Ok(ElectricalComponentCategory::Inverter) + ) + } + + /// Returns true if the component is a PV inverter, false otherwise. + pub fn is_pv_inverter(&self) -> bool { + if let Some(info) = &self.category_specific_info { + if let Some(Kind::Inverter(inverter_info)) = &info.kind { + return matches!( + InverterType::try_from(inverter_info.r#type), + Ok(InverterType::Pv) + ); + } + } + false + } + + /// Returns true if the component is a battery inverter, false otherwise. + pub fn is_battery_inverter(&self) -> bool { + if let Some(info) = &self.category_specific_info { + if let Some(Kind::Inverter(inverter_info)) = &info.kind { + return matches!( + InverterType::try_from(inverter_info.r#type), + Ok(InverterType::Battery) + ); + } + } + false + } + + /// Returns true if the component is a hybrid inverter, false otherwise. + pub fn is_hybrid_inverter(&self) -> bool { + if let Some(info) = &self.category_specific_info { + if let Some(Kind::Inverter(inverter_info)) = &info.kind { + return matches!( + InverterType::try_from(inverter_info.r#type), + Ok(InverterType::Hybrid) + ); + } + } + false + } +}