66import logging
77import re
88import tomllib
9- from dataclasses import dataclass
9+ from dataclasses import field
1010from pathlib import Path
11- from typing import Any , Literal , cast , get_args
11+ from typing import Any , ClassVar , Literal , Self , Type , cast , get_args
12+
13+ from marshmallow import Schema
14+ from marshmallow_dataclass import dataclass
1215
1316_logger = logging .getLogger (__name__ )
1417
2326class ComponentTypeConfig :
2427 """Configuration of a microgrid component type."""
2528
26- component_type : ComponentType
27- """Type of the component."""
28-
2929 meter : list [int ] | None = None
3030 """List of meter IDs for this component."""
3131
@@ -45,12 +45,6 @@ def __post_init__(self) -> None:
4545 self .formula ["AC_ACTIVE_POWER" ] = "+" .join (
4646 [f"#{ cid } " for cid in self ._default_cids ()]
4747 )
48- if self .component_type == "battery" and "BATTERY_SOC_PCT" not in self .formula :
49- if self .component :
50- cids = self .component
51- form = "+" .join ([f"#{ cid } " for cid in cids ])
52- form = f"({ form } )/({ len (cids )} )"
53- self .formula ["BATTERY_SOC_PCT" ] = form
5448
5549 def cids (self , metric : str = "" ) -> list [int ]:
5650 """Get component IDs for this component.
@@ -74,9 +68,7 @@ def cids(self, metric: str = "") -> list[int]:
7468 raise ValueError ("Formula must be a dictionary." )
7569 formula = self .formula .get (metric )
7670 if not formula :
77- raise ValueError (
78- f"{ metric } does not have a formula for { self .component_type } "
79- )
71+ raise ValueError (f"{ metric } does not have a formula" )
8072 # Extract component IDs from the formula which are given as e.g. #123
8173 pattern = r"#(\d+)"
8274 return [int (e ) for e in re .findall (pattern , self .formula [metric ])]
@@ -102,7 +94,7 @@ def _default_cids(self) -> list[int]:
10294 if self .component :
10395 return self .component
10496
105- raise ValueError (f "No IDs available for { self . component_type } " )
97+ raise ValueError ("No IDs available" )
10698
10799 @classmethod
108100 def is_valid_type (cls , ctype : str ) -> bool :
@@ -158,31 +150,17 @@ class BatteryConfig:
158150 """Capacity of the battery in Wh."""
159151
160152
161- @dataclass (frozen = True )
162- class AssetsConfig :
163- """Configuration of the assets in a microgrid."""
164-
165- pv : dict [str , PVConfig ] | None = None
166- """Configuration of the PV system."""
167-
168- wind : dict [str , WindConfig ] | None = None
169- """Configuration of the wind turbines."""
170-
171- battery : dict [str , BatteryConfig ] | None = None
172- """Configuration of the batteries."""
173-
174-
175153# pylint: disable=too-many-instance-attributes
176154@dataclass (frozen = True )
177155class Metadata :
178156 """Metadata for a microgrid."""
179157
158+ microgrid_id : int
159+ """ID of the microgrid."""
160+
180161 name : str | None = None
181162 """Name of the microgrid."""
182163
183- microgrid_id : int | None = None
184- """ID of the microgrid."""
185-
186164 enterprise_id : int | None = None
187165 """Enterprise ID of the microgrid."""
188166
@@ -206,48 +184,24 @@ class Metadata:
206184class MicrogridConfig :
207185 """Configuration of a microgrid."""
208186
209- _metadata : Metadata
187+ meta : Metadata
210188 """Metadata of the microgrid."""
211189
212- _assets_cfg : AssetsConfig
213- """Configuration of the assets in the microgrid ."""
190+ pv : dict [ str , PVConfig ] | None = None
191+ """Configuration of the PV system ."""
214192
215- _component_types_cfg : dict [str , ComponentTypeConfig ]
216- """Mapping of component category types to ac power component config ."""
193+ wind : dict [str , WindConfig ] | None = None
194+ """Configuration of the wind turbines ."""
217195
218- def __init__ ( self , config_dict : dict [str , Any ]) -> None :
219- """Initialize the microgrid configuration.
196+ battery : dict [str , BatteryConfig ] | None = None
197+ """Configuration of the batteries."""
220198
221- Args:
222- config_dict: Dictionary with component type as key and config as value.
223- """
224- self ._metadata = Metadata (** (config_dict .get ("meta" ) or {}))
225-
226- self ._assets_cfg = AssetsConfig (
227- pv = config_dict .get ("pv" ) or {},
228- wind = config_dict .get ("wind" ) or {},
229- battery = config_dict .get ("battery" ) or {},
230- )
231-
232- self ._component_types_cfg = {
233- ctype : ComponentTypeConfig (component_type = cast (ComponentType , ctype ), ** cfg )
234- for ctype , cfg in config_dict .get ("ctype" , {}).items ()
235- if ComponentTypeConfig .is_valid_type (ctype )
236- }
237-
238- @property
239- def meta (self ) -> Metadata :
240- """Return the metadata of the microgrid."""
241- return self ._metadata
242-
243- @property
244- def assets (self ) -> AssetsConfig :
245- """Return the assets configuration of the microgrid."""
246- return self ._assets_cfg
199+ ctype : dict [str , ComponentTypeConfig ] = field (default_factory = dict )
200+ """Mapping of component category types to ac power component config."""
247201
248202 def component_types (self ) -> list [str ]:
249203 """Get a list of all component types in the configuration."""
250- return list (self ._component_types_cfg .keys ())
204+ return list (self .ctype .keys ())
251205
252206 def component_type_ids (
253207 self ,
@@ -271,7 +225,7 @@ def component_type_ids(
271225 ValueError: If the component type is unknown.
272226 KeyError: If `component_category` is invalid.
273227 """
274- cfg = self ._component_types_cfg .get (component_type )
228+ cfg = self .ctype .get (component_type )
275229 if not cfg :
276230 raise ValueError (f"{ component_type } not found in config." )
277231
@@ -300,7 +254,7 @@ def formula(self, component_type: str, metric: str) -> str:
300254 Raises:
301255 ValueError: If the component type is unknown or formula is missing.
302256 """
303- cfg = self ._component_types_cfg .get (component_type )
257+ cfg = self .ctype .get (component_type )
304258 if not cfg :
305259 raise ValueError (f"{ component_type } not found in config." )
306260 if cfg .formula is None :
@@ -311,6 +265,67 @@ def formula(self, component_type: str, metric: str) -> str:
311265
312266 return formula
313267
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 [int , 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+
314329 @staticmethod
315330 def load_configs (
316331 microgrid_config_files : str | Path | list [str | Path ] | None = None ,
@@ -369,14 +384,7 @@ def load_configs(
369384 _logger .warning ("Config path %s is not a file, skipping." , config_path )
370385 continue
371386
372- with config_path .open ("rb" ) as f :
373- cfg_dict = tomllib .load (f )
374- for microgrid_id , mcfg in cfg_dict .items ():
375- _logger .debug (
376- "Loading microgrid config for ID %s from %s" ,
377- microgrid_id ,
378- config_path ,
379- )
380- microgrid_configs [microgrid_id ] = MicrogridConfig (mcfg )
387+ mcfgs = MicrogridConfig .load_from_file (config_path )
388+ microgrid_configs .update ({str (key ): value for key , value in mcfgs .items ()})
381389
382390 return microgrid_configs
0 commit comments