""" plant.py Modello gerarchico completo dell'impianto fotovoltaico. Gerarchia: PVPlant | +-- Inverter | | | +-- CombinerBox | | | +-- PVPanel | +-- PVPanel | +-- ... | +-- Inverter | +-- CombinerBox | +-- PVPanel Il modulo costruisce automaticamente l'intero impianto a partire dal file di configurazione plant.json. Responsabilità: - creazione della gerarchia; - gestione degli inverter; - gestione delle Combiner Box; - creazione automatica dei pannelli; - aggiornamento dell'intero impianto; - aggregazione della potenza; - aggregazione dell'energia; - calcolo della disponibilità; - diagnostica della struttura. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from typing import Dict, List, Any import json from pathlib import Path from .panel import PVPanel from .combiner import CombinerBox from .inverter import Inverter @dataclass class PVPlant: """ Rappresenta l'intero impianto fotovoltaico. """ plant_id: str inverters: List[Inverter] = field( default_factory=list ) total_energy_Wh: float = 0.0 total_dc_energy_Wh: float = 0.0 total_ac_energy_Wh: float = 0.0 # Numero di timestep elaborati timestep_count: int = 0 # ------------------------------------------------------------------ # Costruzione da configurazione # ------------------------------------------------------------------ @classmethod def from_config( cls, config: Dict[str, Any] ) -> "PVPlant": """ Crea un impianto completo leggendo la configurazione. La configurazione deve avere la struttura: layout | +-- inverters | +-- combiners | +-- strings +-- panels_per_string Returns ------- PVPlant Impianto completamente costruito. """ plant_name = ( config[ "plant" ][ "name" ] ) plant = cls( plant_id=plant_name ) module_config = ( config[ "module" ] ) layout_config = ( config[ "layout" ] ) # -------------------------------------------------------------- # Creazione inverter # -------------------------------------------------------------- for inverter_config in ( layout_config[ "inverters" ] ): inverter = ( cls._create_inverter( inverter_config, module_config ) ) plant.inverters.append( inverter ) return plant # ------------------------------------------------------------------ @classmethod def from_json( cls, config_path: str | Path ) -> "PVPlant": """ Carica la configurazione da un file JSON e costruisce l'impianto. """ config_path = Path( config_path ) if not config_path.exists(): raise FileNotFoundError( f"Configurazione non trovata: " f"{config_path}" ) with open( config_path, "r", encoding="utf-8" ) as file: config = json.load( file ) return cls.from_config( config ) # ------------------------------------------------------------------ # Creazione inverter # ------------------------------------------------------------------ @staticmethod def _create_inverter( inverter_config: Dict[str, Any], module_config: Dict[str, Any] ) -> Inverter: """ Crea un inverter e tutte le relative Combiner Box. """ inverter = Inverter( inverter_id= inverter_config[ "id" ], nominal_power_kW= inverter_config[ "nominal_power_kW" ], nominal_efficiency= inverter_config[ "efficiency" ][ "nominal" ], night_consumption_W= inverter_config[ "efficiency" ][ "night_consumption_W" ] ) # -------------------------------------------------------------- # Creazione Combiner Box # -------------------------------------------------------------- for combiner_config in ( inverter_config[ "combiners" ] ): combiner = ( PVPlant._create_combiner( combiner_config, module_config ) ) inverter.add_combiner( combiner ) return inverter # ------------------------------------------------------------------ # Creazione Combiner Box # ------------------------------------------------------------------ @staticmethod def _create_combiner( combiner_config: Dict[str, Any], module_config: Dict[str, Any] ) -> CombinerBox: """ Crea una Combiner Box e tutti i relativi pannelli. La configurazione definisce: strings = numero di stringhe panels_per_string = pannelli per ogni stringa In questa versione la struttura delle stringhe viene rappresentata logicamente creando tutti i pannelli nella Combiner Box. L'ID del pannello contiene: Combiner ID String ID Panel ID """ combiner = CombinerBox( combiner_id= combiner_config[ "id" ] ) strings = ( combiner_config[ "strings" ] ) panels_per_string = ( combiner_config[ "panels_per_string" ] ) # -------------------------------------------------------------- # Parametri pannello # -------------------------------------------------------------- nominal_power = ( module_config[ "nominal_power_W" ] ) electrical = ( module_config[ "electrical" ] ) physical = ( module_config[ "physical" ] ) degradation = ( module_config[ "degradation" ] ) # -------------------------------------------------------------- # Creazione pannelli # -------------------------------------------------------------- for string_number in range( 1, strings + 1 ): for panel_number in range( 1, panels_per_string + 1 ): panel_id = ( f"{combiner_config['id']}" f"_STR_{string_number:02d}" f"_PANEL_{panel_number:02d}" ) panel = PVPanel( panel_id= panel_id, nominal_power= nominal_power, area= physical[ "area_m2" ], vmp= electrical[ "vmp_V" ], imp= electrical[ "imp_A" ], voc= electrical[ "voc_V" ], isc= electrical[ "isc_A" ], temp_coeff= physical[ "temperature_coefficient_power" ], nominal_temp= physical[ "nominal_temperature_C" ], annual_degradation= degradation[ "annual_rate" ] ) combiner.add_panel( panel ) return combiner # ------------------------------------------------------------------ # Gestione inverter # ------------------------------------------------------------------ def add_inverter( self, inverter: Inverter ) -> None: """ Aggiunge un inverter all'impianto. """ self.inverters.append( inverter ) # ------------------------------------------------------------------ def get_inverter_count( self ) -> int: """ Restituisce il numero di inverter. """ return len( self.inverters ) # ------------------------------------------------------------------ def get_active_inverter_count( self ) -> int: """ Restituisce il numero di inverter attivi. """ return sum( 1 for inverter in self.inverters if inverter.enabled ) # ------------------------------------------------------------------ # Conteggi gerarchici # ------------------------------------------------------------------ def get_combiner_count( self ) -> int: """ Restituisce il numero totale di Combiner Box. """ return sum( len( inverter.combiners ) for inverter in self.inverters ) # ------------------------------------------------------------------ def get_panel_count( self ) -> int: """ Restituisce il numero totale di pannelli. """ return sum( len( combiner.panels ) for inverter in self.inverters for combiner in inverter.combiners ) # ------------------------------------------------------------------ def get_active_panel_count( self ) -> int: """ Restituisce il numero di pannelli attivi. """ return sum( 1 for inverter in self.inverters for combiner in inverter.combiners for panel in combiner.panels if panel.enabled ) # ------------------------------------------------------------------ # Aggiornamento impianto # ------------------------------------------------------------------ def update( self, timestamp: datetime, irradiance: float, ambient_temperature: float, years_from_start: float, timestep_minutes: float ) -> Dict[str, Any]: """ Esegue un timestep sull'intero impianto. Flusso: Plant | +-- Inverter | +-- Combiner | +-- Panel Returns ------- Dict Dati aggregati dell'impianto. """ inverter_data = [] # -------------------------------------------------------------- # Aggiornamento inverter # -------------------------------------------------------------- for inverter in self.inverters: data = inverter.update( timestamp= timestamp, irradiance= irradiance, ambient_temperature= ambient_temperature, years_from_start= years_from_start, timestep_minutes= timestep_minutes ) inverter_data.append( data ) # -------------------------------------------------------------- # Aggregazione potenza # -------------------------------------------------------------- total_dc_power = sum( data[ "dc_power_W" ] for data in inverter_data ) total_ac_power = sum( data[ "ac_power_W" ] for data in inverter_data ) # -------------------------------------------------------------- # Aggregazione energia # -------------------------------------------------------------- dc_energy = ( total_dc_power * timestep_minutes / 60.0 ) ac_energy = ( total_ac_power * timestep_minutes / 60.0 ) self.total_dc_energy_Wh += ( dc_energy ) self.total_ac_energy_Wh += ( ac_energy ) self.total_energy_Wh += ( ac_energy ) self.timestep_count += 1 # -------------------------------------------------------------- # Disponibilità # -------------------------------------------------------------- total_inverters = ( self.get_inverter_count() ) active_inverters = ( self.get_active_inverter_count() ) if total_inverters > 0: availability = ( active_inverters / total_inverters ) else: availability = 0.0 # -------------------------------------------------------------- # Output # -------------------------------------------------------------- return { "timestamp": timestamp, "plant_id": self.plant_id, "dc_power_W": total_dc_power, "ac_power_W": total_ac_power, "dc_energy_Wh": dc_energy, "ac_energy_Wh": ac_energy, "total_energy_Wh": self.total_energy_Wh, "dc_total_energy_Wh": self.total_dc_energy_Wh, "ac_total_energy_Wh": self.total_ac_energy_Wh, "inverters": total_inverters, "active_inverters": active_inverters, "combiners": self.get_combiner_count(), "panels": self.get_panel_count(), "active_panels": self.get_active_panel_count(), "availability": availability, "enabled": True } # ------------------------------------------------------------------ # Struttura impianto # ------------------------------------------------------------------ def describe( self ) -> str: """ Restituisce una descrizione testuale della gerarchia dell'impianto. """ lines = [] lines.append( f"PV Plant: " f"{self.plant_id}" ) lines.append( f" Inverters: " f"{self.get_inverter_count()}" ) lines.append( f" Combiners: " f"{self.get_combiner_count()}" ) lines.append( f" Panels: " f"{self.get_panel_count()}" ) lines.append("") for inverter in self.inverters: lines.append( f" ├── " f"{inverter.inverter_id} " f"({inverter.nominal_power_kW} kW)" ) for combiner in ( inverter.combiners ): lines.append( f" │ ├── " f"{combiner.combiner_id} " f"({len(combiner.panels)} panels)" ) # Raggruppamento logico delle stringhe panel_count = ( len( combiner.panels ) ) lines.append( f" │ │ └── " f"{panel_count} panels" ) return "\n".join( lines ) # ------------------------------------------------------------------ # Ricerca componenti # ------------------------------------------------------------------ def find_inverter( self, inverter_id: str ) -> Inverter | None: """ Cerca un inverter tramite ID. """ for inverter in self.inverters: if ( inverter.inverter_id == inverter_id ): return inverter return None # ------------------------------------------------------------------ def find_combiner( self, combiner_id: str ) -> CombinerBox | None: """ Cerca una Combiner Box tramite ID. """ for inverter in self.inverters: for combiner in ( inverter.combiners ): if ( combiner.combiner_id == combiner_id ): return combiner return None # ------------------------------------------------------------------ def find_panel( self, panel_id: str ) -> PVPanel | None: """ Cerca un pannello tramite ID. """ for inverter in self.inverters: for combiner in ( inverter.combiners ): for panel in ( combiner.panels ): if ( panel.panel_id == panel_id ): return panel return None # ------------------------------------------------------------------ def __repr__( self ) -> str: """ Rappresentazione leggibile dell'impianto. """ return ( f"PVPlant(" f"id={self.plant_id}, " f"inverters=" f"{self.get_inverter_count()}, " f"combiners=" f"{self.get_combiner_count()}, " f"panels=" f"{self.get_panel_count()})" ) ```