Skip to content

Commit 3a52b3f

Browse files
authored
Merge pull request #10 from cwasicki/cfg
Add module for microgrid configs
2 parents 29f901b + cdc6543 commit 3a52b3f

File tree

2 files changed

+182
-1
lines changed

2 files changed

+182
-1
lines changed

RELEASE_NOTES.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ The repo provides tools to monitor and maintain solar energy systems with the fo
1111
- **Single Entry Point:** Integrate data fetching, processing and visualisations into a main workflow.
1212
- **Notification Service:** Send alert notifications via email with support for scheduling and retries, including a linear backoff mechanism.
1313

14-
This release provides tools to solar system operators to monitor performance and track trends, and lays the groundwork for identifying potential system issues.
14+
This release provides tools to solar system operators to monitor performance and track trends, and lays the groundwork for identifying potential system issues.
15+
16+
Moreover it includes a microgrid config module that contains component configs to get component IDs and formulas for component types (e.g. PV, battery) and metadata information (e.g. gridpool ID).

src/frequenz/lib/notebooks/config.py

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# License: Proprietary
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Configuration for microgrids."""
5+
6+
import tomllib
7+
from dataclasses import dataclass
8+
from typing import Any, Literal, cast, get_args
9+
10+
ComponentType = Literal["grid", "pv", "battery", "load", "chp"]
11+
"""Valid component types."""
12+
13+
14+
@dataclass
15+
class ComponentTypeConfig:
16+
"""Configuration of a microgrid component type."""
17+
18+
component_type: ComponentType
19+
"""Type of the component."""
20+
21+
meter: list[int] | None = None
22+
"""List of meter IDs for this component."""
23+
24+
inverter: list[int] | None = None
25+
"""List of inverter IDs for this component."""
26+
27+
component: list[int] | None = None
28+
"""List of component IDs for this component."""
29+
30+
formula: str = ""
31+
"""Formula to calculate the power of this component."""
32+
33+
def __post_init__(self) -> None:
34+
"""Set the default formula if none is provided."""
35+
if not self.formula:
36+
self.formula = self._default_formula()
37+
38+
def cids(self) -> list[int]:
39+
"""Get component IDs for this component.
40+
41+
By default, the meter IDs are returned if available, otherwise the inverter IDs.
42+
For components without meters or inverters, the component IDs are returned.
43+
44+
Returns:
45+
List of component IDs for this component.
46+
47+
Raises:
48+
ValueError: If no IDs are available.
49+
"""
50+
if self.meter:
51+
return self.meter
52+
if self.inverter:
53+
return self.inverter
54+
if self.component:
55+
return self.component
56+
57+
raise ValueError(f"No IDs available for {self.component_type}")
58+
59+
def _default_formula(self) -> str:
60+
"""Return the default formula for this component."""
61+
return "+".join([f"#{cid}" for cid in self.cids()])
62+
63+
def has_formula_for(self, metric: str) -> bool:
64+
"""Return whether this formula is valid for a metric."""
65+
return metric in ["AC_ACTIVE_POWER", "AC_REACTIVE_POWER"]
66+
67+
@classmethod
68+
def is_valid_type(cls, ctype: str) -> bool:
69+
"""Check if `ctype` is a valid enum value."""
70+
return ctype in get_args(ComponentType)
71+
72+
73+
@dataclass(frozen=True)
74+
class Metadata:
75+
"""Metadata for a microgrid."""
76+
77+
name: str | None = None
78+
"""Name of the microgrid."""
79+
80+
gid: int | None = None
81+
"""Gridpool ID of the microgrid."""
82+
83+
delivery_area: str | None = None
84+
"""Delivery area of the microgrid."""
85+
86+
87+
@dataclass
88+
class MicrogridConfig:
89+
"""Configuration of a microgrid."""
90+
91+
_metadata: Metadata
92+
"""Metadata of the microgrid."""
93+
94+
_component_types_cfg: dict[str, ComponentTypeConfig]
95+
"""Mapping of component category types to ac power component config."""
96+
97+
def __init__(self, config_dict: dict[str, Any]) -> None:
98+
"""Initialize the microgrid configuration.
99+
100+
Args:
101+
config_dict: Dictionary with component type as key and config as value.
102+
"""
103+
self._metadata = Metadata(**(config_dict.get("meta") or {}))
104+
105+
self._component_types_cfg = {
106+
ctype: ComponentTypeConfig(component_type=cast(ComponentType, ctype), **cfg)
107+
for ctype, cfg in config_dict["ctype"].items()
108+
if ComponentTypeConfig.is_valid_type(ctype)
109+
}
110+
111+
@property
112+
def meta(self) -> Metadata:
113+
"""Return the metadata of the microgrid."""
114+
return self._metadata
115+
116+
def component_types(self) -> list[str]:
117+
"""Get a list of all component types in the configuration."""
118+
return list(self._component_types_cfg.keys())
119+
120+
def component_type_ids(self, component_type: str) -> list[int]:
121+
"""Get a list of all component IDs for a component type.
122+
123+
Args:
124+
component_type: Component type to be aggregated.
125+
126+
Returns:
127+
List of component IDs for this component type.
128+
129+
Raises:
130+
ValueError: If the component type is unknown.
131+
"""
132+
cfg = self._component_types_cfg.get(component_type)
133+
if not cfg:
134+
raise ValueError(f"{component_type} not found in config.")
135+
136+
return cfg.cids()
137+
138+
def formula(self, component_type: str, metric: str) -> str:
139+
"""Get the formula for a component type.
140+
141+
Args:
142+
component_type: Component type to be aggregated.
143+
metric: Metric to be aggregated.
144+
145+
Returns:
146+
Formula to be used for this aggregated component as string.
147+
148+
Raises:
149+
ValueError: If the component type is unknown.
150+
"""
151+
cfg = self._component_types_cfg.get(component_type)
152+
if not cfg:
153+
raise ValueError(f"{component_type} not found in config.")
154+
155+
if not cfg.has_formula_for(metric):
156+
raise ValueError(f"{metric} not supported for {component_type}")
157+
158+
return cfg.formula
159+
160+
@staticmethod
161+
def load_configs(*paths: str) -> dict[str, "MicrogridConfig"]:
162+
"""Load multiple microgrid configurations from a file.
163+
164+
Configs for a single microgrid are expected to be in a single file.
165+
Later files with the same microgrid ID will overwrite the previous configs.
166+
167+
Args:
168+
*paths: Path(es) to the config file(s).
169+
170+
Returns:
171+
Dictionary of single microgrid formula configs with microgrid IDs as keys.
172+
"""
173+
microgrid_configs = {}
174+
for config_path in paths:
175+
with open(config_path, "rb") as f:
176+
cfg_dict = tomllib.load(f)
177+
for microgrid_id, mcfg in cfg_dict.items():
178+
microgrid_configs[microgrid_id] = MicrogridConfig(mcfg)
179+
return microgrid_configs

0 commit comments

Comments
 (0)