frequenz-gridpool 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2025 Frequenz Energy-as-a-Service GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ exclude .cookiecutter-replay.json
2
+ exclude .editorconfig
3
+ exclude .gitignore
4
+ exclude CODEOWNERS
5
+ exclude CONTRIBUTING.md
6
+ exclude mkdocs.yml
7
+ exclude noxfile.py
8
+ exclude src/conftest.py
9
+ recursive-exclude .github *
10
+ recursive-exclude benchmarks *
11
+ recursive-exclude docs *
12
+ recursive-exclude tests *
13
+ recursive-include py *.pyi
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: frequenz-gridpool
3
+ Version: 0.0.1
4
+ Summary: High-level interface to grid pools for the Frequenz platform.
5
+ Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
6
+ License: MIT
7
+ Project-URL: Documentation, https://frequenz-floss.github.io/frequenz-gridpool-python/
8
+ Project-URL: Changelog, https://github.com/frequenz-floss/frequenz-gridpool-python/releases
9
+ Project-URL: Issues, https://github.com/frequenz-floss/frequenz-gridpool-python/issues
10
+ Project-URL: Repository, https://github.com/frequenz-floss/frequenz-gridpool-python
11
+ Project-URL: Support, https://github.com/frequenz-floss/frequenz-gridpool-python/discussions/categories/support
12
+ Keywords: frequenz,python,lib,library,gridpool
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: <4,>=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: marshmallow-dataclass<9,>=8.7.1
24
+ Requires-Dist: typing-extensions<5,>=4.14.1
25
+ Provides-Extra: dev-flake8
26
+ Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
27
+ Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
28
+ Requires-Dist: flake8-pyproject==1.2.3; extra == "dev-flake8"
29
+ Requires-Dist: pydoclint==0.8.1; extra == "dev-flake8"
30
+ Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
31
+ Provides-Extra: dev-formatting
32
+ Requires-Dist: black==25.11.0; extra == "dev-formatting"
33
+ Requires-Dist: isort==6.0.0; extra == "dev-formatting"
34
+ Provides-Extra: dev-mkdocs
35
+ Requires-Dist: Markdown==3.10; extra == "dev-mkdocs"
36
+ Requires-Dist: black==25.11.0; extra == "dev-mkdocs"
37
+ Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
38
+ Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
39
+ Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
40
+ Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
41
+ Requires-Dist: mkdocs-material==9.7.0; extra == "dev-mkdocs"
42
+ Requires-Dist: mkdocstrings[python]==0.30.1; extra == "dev-mkdocs"
43
+ Requires-Dist: mkdocstrings-python==1.19.0; extra == "dev-mkdocs"
44
+ Requires-Dist: frequenz-repo-config[lib]==0.13.6; extra == "dev-mkdocs"
45
+ Provides-Extra: dev-mypy
46
+ Requires-Dist: mypy==1.18.2; extra == "dev-mypy"
47
+ Requires-Dist: types-Markdown==3.10.0.20251106; extra == "dev-mypy"
48
+ Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
49
+ Provides-Extra: dev-noxfile
50
+ Requires-Dist: nox==2025.11.12; extra == "dev-noxfile"
51
+ Requires-Dist: frequenz-repo-config[lib]==0.13.6; extra == "dev-noxfile"
52
+ Provides-Extra: dev-pylint
53
+ Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
54
+ Provides-Extra: dev-pytest
55
+ Requires-Dist: pytest==8.4.1; extra == "dev-pytest"
56
+ Requires-Dist: pylint==4.0.3; extra == "dev-pytest"
57
+ Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.13.6; extra == "dev-pytest"
58
+ Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
59
+ Requires-Dist: pytest-asyncio==1.3.0; extra == "dev-pytest"
60
+ Requires-Dist: async-solipsism==0.8; extra == "dev-pytest"
61
+ Provides-Extra: dev
62
+ Requires-Dist: frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
63
+ Dynamic: license-file
64
+
65
+ # Frequenz Gridpool Library
66
+
67
+ [![Build Status](https://github.com/frequenz-floss/frequenz-gridpool-python/actions/workflows/ci.yaml/badge.svg)](https://github.com/frequenz-floss/frequenz-gridpool-python/actions/workflows/ci.yaml)
68
+ [![PyPI Package](https://img.shields.io/pypi/v/frequenz-gridpool)](https://pypi.org/project/frequenz-gridpool/)
69
+ [![Docs](https://img.shields.io/badge/docs-latest-informational)](https://frequenz-floss.github.io/frequenz-gridpool-python/)
70
+
71
+ ## Introduction
72
+
73
+ High-level interface to grid pools for the Frequenz platform.
74
+
75
+ TODO(cookiecutter): Improve the README file
76
+
77
+ ## Supported Platforms
78
+
79
+ The following platforms are officially supported (tested):
80
+
81
+ - **Python:** 3.11
82
+ - **Operating System:** Ubuntu Linux 20.04
83
+ - **Architectures:** amd64, arm64
84
+
85
+ ## Contributing
86
+
87
+ If you want to know how to build this project and contribute to it, please
88
+ check out the [Contributing Guide](CONTRIBUTING.md).
@@ -0,0 +1,24 @@
1
+ # Frequenz Gridpool Library
2
+
3
+ [![Build Status](https://github.com/frequenz-floss/frequenz-gridpool-python/actions/workflows/ci.yaml/badge.svg)](https://github.com/frequenz-floss/frequenz-gridpool-python/actions/workflows/ci.yaml)
4
+ [![PyPI Package](https://img.shields.io/pypi/v/frequenz-gridpool)](https://pypi.org/project/frequenz-gridpool/)
5
+ [![Docs](https://img.shields.io/badge/docs-latest-informational)](https://frequenz-floss.github.io/frequenz-gridpool-python/)
6
+
7
+ ## Introduction
8
+
9
+ High-level interface to grid pools for the Frequenz platform.
10
+
11
+ TODO(cookiecutter): Improve the README file
12
+
13
+ ## Supported Platforms
14
+
15
+ The following platforms are officially supported (tested):
16
+
17
+ - **Python:** 3.11
18
+ - **Operating System:** Ubuntu Linux 20.04
19
+ - **Architectures:** amd64, arm64
20
+
21
+ ## Contributing
22
+
23
+ If you want to know how to build this project and contribute to it, please
24
+ check out the [Contributing Guide](CONTRIBUTING.md).
@@ -0,0 +1,17 @@
1
+ # Frequenz Gridpool Library Release Notes
2
+
3
+ ## Summary
4
+
5
+ <!-- Here goes a general summary of what this release is about -->
6
+
7
+ ## Upgrading
8
+
9
+ <!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
10
+
11
+ ## New Features
12
+
13
+ * Adds a configuration class for microgrids copied from `frequenz-floss/lib-notebooks`.
14
+
15
+ ## Bug Fixes
16
+
17
+ <!-- Here goes notable bug fixes that are worth a special mention or explanation -->
@@ -0,0 +1,176 @@
1
+ # License: MIT
2
+ # Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3
+
4
+ [build-system]
5
+ requires = [
6
+ "setuptools == 80.9.0",
7
+ "setuptools_scm[toml] == 9.2.2",
8
+ "frequenz-repo-config[lib] == 0.13.6",
9
+ ]
10
+ build-backend = "setuptools.build_meta"
11
+
12
+ [project]
13
+ name = "frequenz-gridpool"
14
+ description = "High-level interface to grid pools for the Frequenz platform."
15
+ readme = "README.md"
16
+ license = { text = "MIT" }
17
+ keywords = ["frequenz", "python", "lib", "library", "gridpool"]
18
+ # TODO(cookiecutter): Remove and add more classifiers if appropriate
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Topic :: Software Development :: Libraries",
26
+ "Typing :: Typed",
27
+ ]
28
+ requires-python = ">= 3.11, < 4"
29
+ # TODO(cookiecutter): Remove and add more dependencies if appropriate
30
+ dependencies = [
31
+ "marshmallow-dataclass>=8.7.1,<9",
32
+ "typing-extensions >= 4.14.1, < 5",
33
+ ]
34
+ dynamic = ["version"]
35
+
36
+ [[project.authors]]
37
+ name = "Frequenz Energy-as-a-Service GmbH"
38
+ email = "floss@frequenz.com"
39
+
40
+ # TODO(cookiecutter): Remove and add more optional dependencies if appropriate
41
+ [project.optional-dependencies]
42
+ dev-flake8 = [
43
+ "flake8 == 7.3.0",
44
+ "flake8-docstrings == 1.7.0",
45
+ "flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
46
+ "pydoclint == 0.8.1",
47
+ "pydocstyle == 6.3.0",
48
+ ]
49
+ dev-formatting = ["black == 25.11.0", "isort == 6.0.0"]
50
+ dev-mkdocs = [
51
+ "Markdown == 3.10",
52
+ "black == 25.11.0",
53
+ "mike == 2.1.3",
54
+ "mkdocs-gen-files == 0.5.0",
55
+ "mkdocs-literate-nav == 0.6.2",
56
+ "mkdocs-macros-plugin == 1.5.0",
57
+ "mkdocs-material == 9.7.0",
58
+ "mkdocstrings[python] == 0.30.1",
59
+ "mkdocstrings-python == 1.19.0",
60
+ "frequenz-repo-config[lib] == 0.13.6",
61
+ ]
62
+ dev-mypy = [
63
+ "mypy == 1.18.2",
64
+ "types-Markdown == 3.10.0.20251106",
65
+ # For checking the noxfile, docs/ script, and tests
66
+ "frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]",
67
+ ]
68
+ dev-noxfile = [
69
+ "nox == 2025.11.12",
70
+ "frequenz-repo-config[lib] == 0.13.6",
71
+ ]
72
+ dev-pylint = [
73
+ # dev-pytest already defines a dependency to pylint because of the examples
74
+ # For checking the noxfile, docs/ script, and tests
75
+ "frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]",
76
+ ]
77
+ dev-pytest = [
78
+ "pytest == 8.4.1",
79
+ "pylint == 4.0.3", # We need this to check for the examples
80
+ "frequenz-repo-config[extra-lint-examples] == 0.13.6",
81
+ "pytest-mock == 3.15.1",
82
+ "pytest-asyncio == 1.3.0",
83
+ "async-solipsism == 0.8",
84
+ ]
85
+ dev = [
86
+ "frequenz-gridpool[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
87
+ ]
88
+
89
+ [project.urls]
90
+ Documentation = "https://frequenz-floss.github.io/frequenz-gridpool-python/"
91
+ Changelog = "https://github.com/frequenz-floss/frequenz-gridpool-python/releases"
92
+ Issues = "https://github.com/frequenz-floss/frequenz-gridpool-python/issues"
93
+ Repository = "https://github.com/frequenz-floss/frequenz-gridpool-python"
94
+ Support = "https://github.com/frequenz-floss/frequenz-gridpool-python/discussions/categories/support"
95
+
96
+ [tool.black]
97
+ line-length = 88
98
+ target-version = ['py311']
99
+ include = '\.pyi?$'
100
+
101
+ [tool.isort]
102
+ profile = "black"
103
+ line_length = 88
104
+ src_paths = ["benchmarks", "examples", "src", "tests"]
105
+
106
+ [tool.flake8]
107
+ # We give some flexibility to go over 88, there are cases like long URLs or
108
+ # code in documenation that have extra indentation. Black will still take care
109
+ # of making everything that can be 88 wide, 88 wide.
110
+ max-line-length = 100
111
+ extend-ignore = [
112
+ "E203", # Whitespace before ':' (conflicts with black)
113
+ "W503", # Line break before binary operator (conflicts with black)
114
+ ]
115
+ # pydoclint options
116
+ style = "google"
117
+ check-return-types = false
118
+ check-yield-types = false
119
+ arg-type-hints-in-docstring = false
120
+ arg-type-hints-in-signature = true
121
+ allow-init-docstring = true
122
+ check-class-attributes = false
123
+
124
+ [tool.pylint.similarities]
125
+ ignore-comments = ['yes']
126
+ ignore-docstrings = ['yes']
127
+ ignore-imports = ['no']
128
+ min-similarity-lines = 40
129
+
130
+ [tool.pylint.messages_control]
131
+ disable = [
132
+ "too-few-public-methods",
133
+ "too-many-return-statements",
134
+ # disabled because it conflicts with isort
135
+ "wrong-import-order",
136
+ "ungrouped-imports",
137
+ # Checked by mypy (and pylint is very flaky checking these)
138
+ "unsubscriptable-object",
139
+ "no-member",
140
+ "no-name-in-module",
141
+ "possibly-used-before-assignment",
142
+ # Checked by flake8
143
+ "f-string-without-interpolation",
144
+ "line-too-long",
145
+ "missing-function-docstring",
146
+ "redefined-outer-name",
147
+ "unnecessary-lambda-assignment",
148
+ "unused-import",
149
+ "unused-variable",
150
+ ]
151
+
152
+ [tool.pytest.ini_options]
153
+ addopts = "-W=all -Werror -Wdefault::DeprecationWarning -Wdefault::PendingDeprecationWarning -vv"
154
+ testpaths = ["tests", "src"]
155
+ asyncio_mode = "auto"
156
+ asyncio_default_fixture_loop_scope = "function"
157
+ required_plugins = ["pytest-asyncio", "pytest-mock"]
158
+
159
+ [tool.mypy]
160
+ explicit_package_bases = true
161
+ namespace_packages = true
162
+ # This option disables mypy cache, and it is sometimes useful to enable it if
163
+ # you are getting weird intermittent error, or error in the CI but not locally
164
+ # (or vice versa). In particular errors saying that type: ignore is not
165
+ # used but getting the original ignored error when removing the type: ignore.
166
+ # See for example: https://github.com/python/mypy/issues/2960
167
+ #no_incremental = true
168
+ packages = ["frequenz.gridpool"]
169
+ strict = true
170
+
171
+ [[tool.mypy.overrides]]
172
+ module = ["mkdocs_macros.*", "sybil", "sybil.*"]
173
+ ignore_missing_imports = true
174
+
175
+ [tool.setuptools_scm]
176
+ version_scheme = "post-release"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ # License: MIT
2
+ # Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """High-level interface to grid pools for the Frequenz platform."""
5
+
6
+ from ._microgrid_config import MicrogridConfig
7
+
8
+ __all__ = ["MicrogridConfig"]
@@ -0,0 +1,390 @@
1
+ # License: MIT
2
+ # Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Configuration for microgrids."""
5
+
6
+ import logging
7
+ import re
8
+ import tomllib
9
+ from dataclasses import field
10
+ from pathlib import Path
11
+ from typing import Any, ClassVar, Literal, Self, Type, cast, get_args
12
+
13
+ from marshmallow import Schema
14
+ from marshmallow_dataclass import dataclass
15
+
16
+ _logger = logging.getLogger(__name__)
17
+
18
+ ComponentType = Literal["grid", "pv", "battery", "consumption", "chp", "ev"]
19
+ """Valid component types."""
20
+
21
+ ComponentCategory = Literal["meter", "inverter", "component"]
22
+ """Valid component categories."""
23
+
24
+
25
+ @dataclass
26
+ class ComponentTypeConfig:
27
+ """Configuration of a microgrid component type."""
28
+
29
+ meter: list[int] | None = None
30
+ """List of meter IDs for this component."""
31
+
32
+ inverter: list[int] | None = None
33
+ """List of inverter IDs for this component."""
34
+
35
+ component: list[int] | None = None
36
+ """List of component IDs for this component."""
37
+
38
+ formula: dict[str, str] | None = None
39
+ """Formula to calculate the power of this component."""
40
+
41
+ def __post_init__(self) -> None:
42
+ """Set the default formula if none is provided."""
43
+ self.formula = self.formula or {}
44
+ if "AC_ACTIVE_POWER" not in self.formula:
45
+ self.formula["AC_ACTIVE_POWER"] = "+".join(
46
+ [f"#{cid}" for cid in self._default_cids()]
47
+ )
48
+
49
+ def cids(self, metric: str = "") -> list[int]:
50
+ """Get component IDs for this component.
51
+
52
+ By default, the meter IDs are returned if available, otherwise the inverter IDs.
53
+ For components without meters or inverters, the component IDs are returned.
54
+
55
+ If a metric is provided, the component IDs are extracted from the formula.
56
+
57
+ Args:
58
+ metric: Metric name of the formula.
59
+
60
+ Returns:
61
+ List of component IDs for this component.
62
+
63
+ Raises:
64
+ ValueError: If the metric is not supported or improperly set.
65
+ """
66
+ if metric:
67
+ if not isinstance(self.formula, dict):
68
+ raise ValueError("Formula must be a dictionary.")
69
+ formula = self.formula.get(metric)
70
+ if not formula:
71
+ raise ValueError(f"{metric} does not have a formula")
72
+ # Extract component IDs from the formula which are given as e.g. #123
73
+ pattern = r"#(\d+)"
74
+ return [int(e) for e in re.findall(pattern, self.formula[metric])]
75
+
76
+ return self._default_cids()
77
+
78
+ def _default_cids(self) -> list[int]:
79
+ """Get the default component IDs for this component.
80
+
81
+ If available, the meter IDs are returned, otherwise the inverter IDs.
82
+ For components without meters or inverters, the component IDs are returned.
83
+
84
+ Returns:
85
+ List of component IDs for this component.
86
+
87
+ Raises:
88
+ ValueError: If no IDs are available.
89
+ """
90
+ if self.meter:
91
+ return self.meter
92
+ if self.inverter:
93
+ return self.inverter
94
+ if self.component:
95
+ return self.component
96
+
97
+ raise ValueError("No IDs available")
98
+
99
+ @classmethod
100
+ def is_valid_type(cls, ctype: str) -> bool:
101
+ """Check if `ctype` is a valid enum value."""
102
+ return ctype in get_args(ComponentType)
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class PVConfig:
107
+ """Configuration of a PV system in a microgrid."""
108
+
109
+ peak_power: float | None = None
110
+ """Peak power of the PV system in Watt."""
111
+
112
+ rated_power: float | None = None
113
+ """Rated power of the inverters in Watt."""
114
+
115
+ curtailable: bool = False
116
+ """Flag to indicate if PV system can be curtailed."""
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class WindConfig:
121
+ """Configuration of a wind turbine in a microgrid."""
122
+
123
+ turbine_model: str | None = None
124
+ """Model name of the wind turbine."""
125
+
126
+ rated_power: float | None = None
127
+ """Rated power of the wind turbine in Watt."""
128
+
129
+ turbine_height: float | None = None
130
+ """Height of the wind turbine in meters."""
131
+
132
+ number_of_turbines: int = 1
133
+ """Number of wind turbines."""
134
+
135
+ hellmann_exponent: float | None = None
136
+ """Hellmann exponent for wind speed extrapolation. See: https://w.wiki/FMw9"""
137
+
138
+ longitude: float | None = None
139
+ """Geographic longitude of the wind turbine."""
140
+
141
+ latitude: float | None = None
142
+ """Geographic latitude of the wind turbine."""
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class BatteryConfig:
147
+ """Configuration of a battery in a microgrid."""
148
+
149
+ capacity: float | None = None
150
+ """Capacity of the battery in Wh."""
151
+
152
+
153
+ # pylint: disable=too-many-instance-attributes
154
+ @dataclass(frozen=True)
155
+ class Metadata:
156
+ """Metadata for a microgrid."""
157
+
158
+ microgrid_id: int
159
+ """ID of the microgrid."""
160
+
161
+ name: str | None = None
162
+ """Name of the microgrid."""
163
+
164
+ enterprise_id: int | None = None
165
+ """Enterprise ID of the microgrid."""
166
+
167
+ gid: int | None = None
168
+ """Gridpool ID of the microgrid."""
169
+
170
+ delivery_area: str | None = None
171
+ """Delivery area of the microgrid."""
172
+
173
+ latitude: float | None = None
174
+ """Geographic latitude of the microgrid."""
175
+
176
+ longitude: float | None = None
177
+ """Geographic longitude of the microgrid."""
178
+
179
+ altitude: float | None = None
180
+ """Geographic altitude of the microgrid."""
181
+
182
+
183
+ @dataclass
184
+ class MicrogridConfig:
185
+ """Configuration of a microgrid."""
186
+
187
+ meta: Metadata
188
+ """Metadata of the microgrid."""
189
+
190
+ pv: dict[str, PVConfig] | None = None
191
+ """Configuration of the PV system."""
192
+
193
+ wind: dict[str, WindConfig] | None = None
194
+ """Configuration of the wind turbines."""
195
+
196
+ battery: dict[str, BatteryConfig] | None = None
197
+ """Configuration of the batteries."""
198
+
199
+ ctype: dict[str, ComponentTypeConfig] = field(default_factory=dict)
200
+ """Mapping of component category types to ac power component config."""
201
+
202
+ def component_types(self) -> list[str]:
203
+ """Get a list of all component types in the configuration."""
204
+ return list(self.ctype.keys())
205
+
206
+ def component_type_ids(
207
+ self,
208
+ component_type: str,
209
+ component_category: str | None = None,
210
+ metric: str = "",
211
+ ) -> list[int]:
212
+ """Get a list of all component IDs for a component type.
213
+
214
+ Args:
215
+ component_type: Component type to be aggregated.
216
+ component_category: Specific category of component IDs to retrieve
217
+ (e.g., "meter", "inverter", or "component"). If not provided,
218
+ the default logic is used.
219
+ metric: Metric name of the formula if CIDs should be extracted from the formula.
220
+
221
+ Returns:
222
+ List of component IDs for this component type.
223
+
224
+ Raises:
225
+ ValueError: If the component type is unknown.
226
+ KeyError: If `component_category` is invalid.
227
+ """
228
+ cfg = self.ctype.get(component_type)
229
+ if not cfg:
230
+ raise ValueError(f"{component_type} not found in config.")
231
+
232
+ if component_category:
233
+ valid_categories = get_args(ComponentCategory)
234
+ if component_category not in valid_categories:
235
+ raise KeyError(
236
+ f"Invalid component category: {component_category}. "
237
+ f"Valid categories are {valid_categories}"
238
+ )
239
+ category_ids = cast(list[int], getattr(cfg, component_category, []))
240
+ return category_ids
241
+
242
+ return cfg.cids(metric)
243
+
244
+ def formula(self, component_type: str, metric: str) -> str:
245
+ """Get the formula for a component type.
246
+
247
+ Args:
248
+ component_type: Component type to be aggregated.
249
+ metric: Metric to be aggregated.
250
+
251
+ Returns:
252
+ Formula to be used for this aggregated component as string.
253
+
254
+ Raises:
255
+ ValueError: If the component type is unknown or formula is missing.
256
+ """
257
+ cfg = self.ctype.get(component_type)
258
+ if not cfg:
259
+ raise ValueError(f"{component_type} not found in config.")
260
+ if cfg.formula is None:
261
+ raise ValueError(f"No formula set for {component_type}")
262
+ formula = cfg.formula.get(metric)
263
+ if not formula:
264
+ raise ValueError(f"{component_type} is missing formula for {metric}")
265
+
266
+ return formula
267
+
268
+ Schema: ClassVar[Type[Schema]] = Schema
269
+
270
+ @classmethod
271
+ def _load_table_entries(cls, data: dict[str, Any]) -> dict[str, Self]:
272
+ """Load microgrid configurations from table entries.
273
+
274
+ Args:
275
+ data: The loaded TOML data.
276
+
277
+ Returns:
278
+ A dict mapping microgrid IDs to MicrogridConfig instances.
279
+
280
+ Raises:
281
+ ValueError: If top-level keys are not numeric microgrid IDs
282
+ or if there is a microgrid ID mismatch.
283
+ TypeError: If microgrid data is not a dict.
284
+ """
285
+ if not all(str(k).isdigit() for k in data.keys()):
286
+ raise ValueError("All top-level keys must be numeric microgrid IDs.")
287
+
288
+ mgrids = {}
289
+ for mid, entry in data.items():
290
+ if not mid.isdigit():
291
+ raise ValueError(
292
+ f"Table reader: Microgrid ID key must be numeric, got {mid}"
293
+ )
294
+ if not isinstance(entry, dict):
295
+ raise TypeError("Table reader: Each microgrid entry must be a dict")
296
+
297
+ mgrid = cls.Schema().load(entry)
298
+ if mgrid.meta is None or mgrid.meta.microgrid_id is None:
299
+ raise ValueError(
300
+ "Table reader: Each microgrid entry must have a meta.microgrid_id"
301
+ )
302
+ if int(mgrid.meta.microgrid_id) != int(mid):
303
+ raise ValueError(
304
+ f"Table reader: Microgrid ID mismatch: key {mid} != {mgrid.meta.microgrid_id}"
305
+ )
306
+
307
+ mgrids[mid] = mgrid
308
+
309
+ return mgrids
310
+
311
+ @classmethod
312
+ def load_from_file(cls, config_path: Path) -> dict[str, Self]:
313
+ """
314
+ Load and validate configuration settings from a TOML file.
315
+
316
+ Args:
317
+ config_path: the path to the TOML configuration file.
318
+
319
+ Returns:
320
+ A dict mapping microgrid IDs to MicrogridConfig instances.
321
+ """
322
+ with config_path.open("rb") as f:
323
+ data = tomllib.load(f)
324
+
325
+ assert isinstance(data, dict)
326
+
327
+ return cls._load_table_entries(data)
328
+
329
+ @staticmethod
330
+ def load_configs(
331
+ microgrid_config_files: str | Path | list[str | Path] | None = None,
332
+ microgrid_config_dir: str | Path | None = None,
333
+ ) -> dict[str, "MicrogridConfig"]:
334
+ """Load multiple microgrid configurations from a file.
335
+
336
+ Configs for a single microgrid are expected to be in a single file.
337
+ Later files with the same microgrid ID will overwrite the previous configs.
338
+
339
+ Args:
340
+ microgrid_config_files: Path to a single microgrid config file or list of paths.
341
+ microgrid_config_dir: Directory containing multiple microgrid config files.
342
+
343
+ Returns:
344
+ Dictionary of single microgrid formula configs with microgrid IDs as keys.
345
+
346
+ Raises:
347
+ ValueError: If no config files or dir is provided, or if no config files are found.
348
+ """
349
+ if microgrid_config_files is None and microgrid_config_dir is None:
350
+ raise ValueError(
351
+ "No microgrid config path or directory provided. "
352
+ "Please provide at least one."
353
+ )
354
+
355
+ config_files: list[Path] = []
356
+
357
+ if microgrid_config_files:
358
+ if isinstance(microgrid_config_files, str):
359
+ config_files = [Path(microgrid_config_files)]
360
+ elif isinstance(microgrid_config_files, Path):
361
+ config_files = [microgrid_config_files]
362
+ elif isinstance(microgrid_config_files, list):
363
+ config_files = [Path(f) for f in microgrid_config_files]
364
+
365
+ if microgrid_config_dir:
366
+ if Path(microgrid_config_dir).is_dir():
367
+ config_files += list(Path(microgrid_config_dir).glob("*.toml"))
368
+ else:
369
+ raise ValueError(
370
+ f"Microgrid config directory {microgrid_config_dir} "
371
+ "is not a directory"
372
+ )
373
+
374
+ if len(config_files) == 0:
375
+ raise ValueError(
376
+ "No microgrid config files found. "
377
+ "Please provide at least one valid config file."
378
+ )
379
+
380
+ microgrid_configs: dict[str, "MicrogridConfig"] = {}
381
+
382
+ for config_path in config_files:
383
+ if not config_path.is_file():
384
+ _logger.warning("Config path %s is not a file, skipping.", config_path)
385
+ continue
386
+
387
+ mcfgs = MicrogridConfig.load_from_file(config_path)
388
+ microgrid_configs.update({str(key): value for key, value in mcfgs.items()})
389
+
390
+ return microgrid_configs
@@ -0,0 +1,13 @@
1
+ # License: MIT
2
+ # Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Validate docstring code examples.
5
+
6
+ Code examples are often wrapped in triple backticks (```) within docstrings.
7
+ This plugin extracts these code examples and validates them using pylint.
8
+ """
9
+
10
+ from frequenz.repo.config.pytest import examples
11
+ from sybil import Sybil
12
+
13
+ pytest_collect_file = Sybil(**examples.get_sybil_arguments()).pytest()
File without changes
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: frequenz-gridpool
3
+ Version: 0.0.1
4
+ Summary: High-level interface to grid pools for the Frequenz platform.
5
+ Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
6
+ License: MIT
7
+ Project-URL: Documentation, https://frequenz-floss.github.io/frequenz-gridpool-python/
8
+ Project-URL: Changelog, https://github.com/frequenz-floss/frequenz-gridpool-python/releases
9
+ Project-URL: Issues, https://github.com/frequenz-floss/frequenz-gridpool-python/issues
10
+ Project-URL: Repository, https://github.com/frequenz-floss/frequenz-gridpool-python
11
+ Project-URL: Support, https://github.com/frequenz-floss/frequenz-gridpool-python/discussions/categories/support
12
+ Keywords: frequenz,python,lib,library,gridpool
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: <4,>=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: marshmallow-dataclass<9,>=8.7.1
24
+ Requires-Dist: typing-extensions<5,>=4.14.1
25
+ Provides-Extra: dev-flake8
26
+ Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
27
+ Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
28
+ Requires-Dist: flake8-pyproject==1.2.3; extra == "dev-flake8"
29
+ Requires-Dist: pydoclint==0.8.1; extra == "dev-flake8"
30
+ Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
31
+ Provides-Extra: dev-formatting
32
+ Requires-Dist: black==25.11.0; extra == "dev-formatting"
33
+ Requires-Dist: isort==6.0.0; extra == "dev-formatting"
34
+ Provides-Extra: dev-mkdocs
35
+ Requires-Dist: Markdown==3.10; extra == "dev-mkdocs"
36
+ Requires-Dist: black==25.11.0; extra == "dev-mkdocs"
37
+ Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
38
+ Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
39
+ Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
40
+ Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
41
+ Requires-Dist: mkdocs-material==9.7.0; extra == "dev-mkdocs"
42
+ Requires-Dist: mkdocstrings[python]==0.30.1; extra == "dev-mkdocs"
43
+ Requires-Dist: mkdocstrings-python==1.19.0; extra == "dev-mkdocs"
44
+ Requires-Dist: frequenz-repo-config[lib]==0.13.6; extra == "dev-mkdocs"
45
+ Provides-Extra: dev-mypy
46
+ Requires-Dist: mypy==1.18.2; extra == "dev-mypy"
47
+ Requires-Dist: types-Markdown==3.10.0.20251106; extra == "dev-mypy"
48
+ Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
49
+ Provides-Extra: dev-noxfile
50
+ Requires-Dist: nox==2025.11.12; extra == "dev-noxfile"
51
+ Requires-Dist: frequenz-repo-config[lib]==0.13.6; extra == "dev-noxfile"
52
+ Provides-Extra: dev-pylint
53
+ Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
54
+ Provides-Extra: dev-pytest
55
+ Requires-Dist: pytest==8.4.1; extra == "dev-pytest"
56
+ Requires-Dist: pylint==4.0.3; extra == "dev-pytest"
57
+ Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.13.6; extra == "dev-pytest"
58
+ Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
59
+ Requires-Dist: pytest-asyncio==1.3.0; extra == "dev-pytest"
60
+ Requires-Dist: async-solipsism==0.8; extra == "dev-pytest"
61
+ Provides-Extra: dev
62
+ Requires-Dist: frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
63
+ Dynamic: license-file
64
+
65
+ # Frequenz Gridpool Library
66
+
67
+ [![Build Status](https://github.com/frequenz-floss/frequenz-gridpool-python/actions/workflows/ci.yaml/badge.svg)](https://github.com/frequenz-floss/frequenz-gridpool-python/actions/workflows/ci.yaml)
68
+ [![PyPI Package](https://img.shields.io/pypi/v/frequenz-gridpool)](https://pypi.org/project/frequenz-gridpool/)
69
+ [![Docs](https://img.shields.io/badge/docs-latest-informational)](https://frequenz-floss.github.io/frequenz-gridpool-python/)
70
+
71
+ ## Introduction
72
+
73
+ High-level interface to grid pools for the Frequenz platform.
74
+
75
+ TODO(cookiecutter): Improve the README file
76
+
77
+ ## Supported Platforms
78
+
79
+ The following platforms are officially supported (tested):
80
+
81
+ - **Python:** 3.11
82
+ - **Operating System:** Ubuntu Linux 20.04
83
+ - **Architectures:** amd64, arm64
84
+
85
+ ## Contributing
86
+
87
+ If you want to know how to build this project and contribute to it, please
88
+ check out the [Contributing Guide](CONTRIBUTING.md).
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ RELEASE_NOTES.md
5
+ pyproject.toml
6
+ src/frequenz/gridpool/__init__.py
7
+ src/frequenz/gridpool/_microgrid_config.py
8
+ src/frequenz/gridpool/conftest.py
9
+ src/frequenz/gridpool/py.typed
10
+ src/frequenz_gridpool.egg-info/PKG-INFO
11
+ src/frequenz_gridpool.egg-info/SOURCES.txt
12
+ src/frequenz_gridpool.egg-info/dependency_links.txt
13
+ src/frequenz_gridpool.egg-info/requires.txt
14
+ src/frequenz_gridpool.egg-info/top_level.txt
@@ -0,0 +1,48 @@
1
+ marshmallow-dataclass<9,>=8.7.1
2
+ typing-extensions<5,>=4.14.1
3
+
4
+ [dev]
5
+ frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]
6
+
7
+ [dev-flake8]
8
+ flake8==7.3.0
9
+ flake8-docstrings==1.7.0
10
+ flake8-pyproject==1.2.3
11
+ pydoclint==0.8.1
12
+ pydocstyle==6.3.0
13
+
14
+ [dev-formatting]
15
+ black==25.11.0
16
+ isort==6.0.0
17
+
18
+ [dev-mkdocs]
19
+ Markdown==3.10
20
+ black==25.11.0
21
+ mike==2.1.3
22
+ mkdocs-gen-files==0.5.0
23
+ mkdocs-literate-nav==0.6.2
24
+ mkdocs-macros-plugin==1.5.0
25
+ mkdocs-material==9.7.0
26
+ mkdocstrings[python]==0.30.1
27
+ mkdocstrings-python==1.19.0
28
+ frequenz-repo-config[lib]==0.13.6
29
+
30
+ [dev-mypy]
31
+ mypy==1.18.2
32
+ types-Markdown==3.10.0.20251106
33
+ frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]
34
+
35
+ [dev-noxfile]
36
+ nox==2025.11.12
37
+ frequenz-repo-config[lib]==0.13.6
38
+
39
+ [dev-pylint]
40
+ frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]
41
+
42
+ [dev-pytest]
43
+ pytest==8.4.1
44
+ pylint==4.0.3
45
+ frequenz-repo-config[extra-lint-examples]==0.13.6
46
+ pytest-mock==3.15.1
47
+ pytest-asyncio==1.3.0
48
+ async-solipsism==0.8