Skip to content

Commit 7b1bfcb

Browse files
committed
Implement the pyo3 based wrapper for the component graph
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 4c1f2b7 commit 7b1bfcb

File tree

8 files changed

+840
-0
lines changed

8 files changed

+840
-0
lines changed

Cargo.lock

Lines changed: 258 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "frequenz-microgrid-component-graph-python-bindings"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
[lib]
8+
name = "frequenz_microgrid_component_graph_python_bindings"
9+
crate-type = ["cdylib"]
10+
11+
[dependencies]
12+
pyo3 = "0.27.1"
13+
frequenz-microgrid-component-graph = "0.1.2"

src/category.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// License: MIT
2+
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
use frequenz_microgrid_component_graph as cg;
5+
use pyo3::{exceptions, prelude::*};
6+
7+
struct ComponentClasses<'py> {
8+
grid_connection_point: Bound<'py, PyAny>,
9+
meter: Bound<'py, PyAny>,
10+
battery: Bound<'py, PyAny>,
11+
ev_charger: Bound<'py, PyAny>,
12+
chp: Bound<'py, PyAny>,
13+
battery_inverter: Bound<'py, PyAny>,
14+
solar_inverter: Bound<'py, PyAny>,
15+
hybrid_inverter: Bound<'py, PyAny>,
16+
unspecified_component: Bound<'py, PyAny>,
17+
}
18+
19+
impl<'py> ComponentClasses<'py> {
20+
fn try_new(py: Python<'py>) -> PyResult<Self> {
21+
let candidates = vec![
22+
"frequenz.client.microgrid.component".to_string(),
23+
"frequenz.client.assets.electrical_component".to_string(),
24+
];
25+
26+
let mut last_err: Option<PyErr> = None;
27+
for path in &candidates {
28+
match py.import(path) {
29+
Ok(module) => {
30+
return Ok(Self {
31+
grid_connection_point: module.getattr("GridConnectionPoint")?,
32+
meter: module.getattr("Meter")?,
33+
battery: module.getattr("Battery")?,
34+
ev_charger: module.getattr("EvCharger")?,
35+
chp: module.getattr("Chp")?,
36+
battery_inverter: module.getattr("BatteryInverter")?,
37+
solar_inverter: module.getattr("SolarInverter")?,
38+
hybrid_inverter: module.getattr("HybridInverter")?,
39+
unspecified_component: module.getattr("UnspecifiedComponent")?,
40+
});
41+
}
42+
Err(e) => last_err = Some(e),
43+
}
44+
}
45+
Err(pyo3::exceptions::PyImportError::new_err(format!(
46+
"Could not import a component provider. Tried: {candidates:?}. \
47+
Install one: pip install frequenz-component-graph[microgrid] or [assets]. \
48+
Last error: {last_err:?}"
49+
)))
50+
}
51+
}
52+
53+
pub(crate) fn category_from_python_component(
54+
py: Python<'_>,
55+
object: &Bound<'_, PyAny>,
56+
) -> PyResult<cg::ComponentCategory> {
57+
let comp_classes = ComponentClasses::try_new(py)?;
58+
59+
if object.is_instance(&comp_classes.grid_connection_point)?
60+
|| object.is(&comp_classes.grid_connection_point)
61+
{
62+
Ok(cg::ComponentCategory::GridConnectionPoint)
63+
} else if object.is_instance(&comp_classes.meter)? || object.is(&comp_classes.meter) {
64+
Ok(cg::ComponentCategory::Meter)
65+
} else if object.is_instance(&comp_classes.battery)? || object.is(&comp_classes.battery) {
66+
Ok(cg::ComponentCategory::Battery(cg::BatteryType::Unspecified))
67+
} else if object.is_instance(&comp_classes.ev_charger)? || object.is(&comp_classes.ev_charger) {
68+
Ok(cg::ComponentCategory::EvCharger(
69+
cg::EvChargerType::Unspecified,
70+
))
71+
} else if object.is_instance(&comp_classes.chp)? || object.is(&comp_classes.chp) {
72+
Ok(cg::ComponentCategory::Chp)
73+
} else if object.is_instance(&comp_classes.battery_inverter)?
74+
|| object.is(&comp_classes.battery_inverter)
75+
{
76+
Ok(cg::ComponentCategory::Inverter(cg::InverterType::Battery))
77+
} else if object.is_instance(&comp_classes.solar_inverter)?
78+
|| object.is(&comp_classes.solar_inverter)
79+
{
80+
Ok(cg::ComponentCategory::Inverter(cg::InverterType::Pv))
81+
} else if object.is_instance(&comp_classes.hybrid_inverter)?
82+
|| object.is(&comp_classes.hybrid_inverter)
83+
{
84+
Ok(cg::ComponentCategory::Inverter(cg::InverterType::Hybrid))
85+
} else if object.is_instance(&comp_classes.unspecified_component)?
86+
|| object.is(&comp_classes.unspecified_component)
87+
{
88+
Ok(cg::ComponentCategory::Unspecified)
89+
} else {
90+
Err(exceptions::PyValueError::new_err(format!(
91+
"Unsupported component category: {:?}",
92+
object
93+
)))
94+
}
95+
}
96+
97+
pub(crate) fn match_category(
98+
category_1: cg::ComponentCategory,
99+
category_2: cg::ComponentCategory,
100+
) -> bool {
101+
match (category_1, category_2) {
102+
(cg::ComponentCategory::Inverter(type_1), cg::ComponentCategory::Inverter(type_2)) => {
103+
match (type_1, type_2) {
104+
(cg::InverterType::Unspecified, _) | (_, cg::InverterType::Unspecified) => true,
105+
_ => type_1 == type_2,
106+
}
107+
}
108+
(cg::ComponentCategory::Battery(type_1), cg::ComponentCategory::Battery(type_2)) => {
109+
match (type_1, type_2) {
110+
(cg::BatteryType::Unspecified, _) | (_, cg::BatteryType::Unspecified) => true,
111+
_ => type_1 == type_2,
112+
}
113+
}
114+
(cg::ComponentCategory::EvCharger(type_1), cg::ComponentCategory::EvCharger(type_2)) => {
115+
match (type_1, type_2) {
116+
(cg::EvChargerType::Unspecified, _) | (_, cg::EvChargerType::Unspecified) => true,
117+
_ => type_1 == type_2,
118+
}
119+
}
120+
_ => category_1 == category_2,
121+
}
122+
}

0 commit comments

Comments
 (0)