""" combiner.py Modello di una Combiner Box fotovoltaica. La Combiner Box raccoglie la produzione di più pannelli fotovoltaici e fornisce una misura aggregata verso l'inverter. Gerarchia: PV Plant | +-- Inverter | +-- Combiner Box | +-- Panel +-- Panel +-- ... Responsabilità principali: - gestione dei pannelli collegati; - aggregazione della potenza DC; - aggregazione della corrente; - calcolo della tensione equivalente; - conteggio dei pannelli attivi; - simulazione di guasto della combiner; - simulazione delle perdite elettriche; - simulazione della temperatura interna; - accumulo dell'energia prodotta. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from typing import Dict, List import random from .panel import PVPanel @dataclass class CombinerBox: """ Rappresenta una Combiner Box. Parameters ---------- combiner_id: Identificativo univoco della combiner. panels: Lista dei pannelli collegati alla combiner. nominal_voltage: Tensione nominale del bus DC [V]. cable_loss_factor: Fattore di perdita dovuto ai cavi e alle connessioni. Esempio: 0.99 significa una perdita dell'1%. sensor_noise: Rumore relativo delle misure. enabled: Stato operativo della combiner. internal_temperature: Temperatura interna simulata della combiner [°C]. """ combiner_id: str panels: List[PVPanel] = field(default_factory=list) nominal_voltage: float = 800.0 cable_loss_factor: float = 0.99 sensor_noise: float = 0.005 enabled: bool = True internal_temperature: float = 25.0 total_energy_Wh: float = 0.0 # ------------------------------------------------------------------ # Gestione pannelli # ------------------------------------------------------------------ def add_panel(self, panel: PVPanel) -> None: """ Aggiunge un pannello alla Combiner Box. """ self.panels.append(panel) # ------------------------------------------------------------------ def remove_panel(self, panel_id: str) -> None: """ Rimuove un pannello dalla Combiner Box utilizzando il suo ID. """ self.panels = [ panel for panel in self.panels if panel.panel_id != panel_id ] # ------------------------------------------------------------------ def get_panel_count(self) -> int: """ Restituisce il numero totale di pannelli collegati. """ return len(self.panels) # ------------------------------------------------------------------ def get_active_panel_count(self) -> int: """ Restituisce il numero di pannelli attualmente attivi. """ return sum( 1 for panel in self.panels if panel.enabled ) # ------------------------------------------------------------------ # Calcolo grandezze elettriche # ------------------------------------------------------------------ def calculate_voltage( self, panel_data: List[Dict] ) -> float: """ Calcola la tensione equivalente della Combiner Box. Per una simulazione semplificata viene utilizzata la tensione media dei pannelli attivi. In una futura versione sarà possibile modellare esplicitamente stringhe serie/parallelo. """ active_voltages = [ data["voltage_V"] for data in panel_data if data["voltage_V"] > 0 ] if not active_voltages: return 0.0 average_voltage = ( sum(active_voltages) / len(active_voltages) ) # Rumore del sensore noise = random.gauss( 1.0, self.sensor_noise ) return average_voltage * noise # ------------------------------------------------------------------ def calculate_current( self, panel_data: List[Dict] ) -> float: """ Calcola la corrente totale della Combiner Box. La corrente dei pannelli viene sommata. """ total_current = sum( data["current_A"] for data in panel_data ) return total_current # ------------------------------------------------------------------ def calculate_power( self, panel_data: List[Dict] ) -> float: """ Calcola la potenza DC aggregata. La potenza dei pannelli viene sommata e successivamente vengono applicate le perdite dei cavi e delle connessioni. """ raw_power = sum( data["power_W"] for data in panel_data ) power_after_losses = ( raw_power * self.cable_loss_factor ) if power_after_losses <= 0: return 0.0 # Rumore della misura noise = random.gauss( 1.0, self.sensor_noise ) return max( 0.0, power_after_losses * noise ) # ------------------------------------------------------------------ def calculate_temperature( self, panel_data: List[Dict], ambient_temperature: float ) -> float: """ Stima la temperatura interna della Combiner Box. La temperatura aumenta in funzione della potenza dissipata e della temperatura ambiente. """ power = sum( data["power_W"] for data in panel_data ) # Riscaldamento semplificato dell'elettronica thermal_effect = ( power / 10000.0 ) temperature = ( ambient_temperature + thermal_effect ) return temperature # ------------------------------------------------------------------ # Ciclo principale # ------------------------------------------------------------------ def update( self, timestamp: datetime, irradiance: float, ambient_temperature: float, years_from_start: float, timestep_minutes: float ) -> Dict: """ Aggiorna lo stato della Combiner Box. Per ogni pannello: 1. viene chiamato panel.update(); 2. i dati vengono raccolti; 3. le grandezze vengono aggregate; 4. viene restituito un dizionario con le misure. Returns ------- Dict Dati aggregati della Combiner Box. """ # -------------------------------------------------------------- # Combiner offline # -------------------------------------------------------------- if not self.enabled: return { "timestamp": timestamp, "combiner_id": self.combiner_id, "power_W": 0.0, "current_A": 0.0, "voltage_V": 0.0, "energy_Wh": 0.0, "total_energy_Wh": self.total_energy_Wh, "active_panels": 0, "total_panels": self.get_panel_count(), "internal_temperature_C": ambient_temperature, "enabled": False } # -------------------------------------------------------------- # Aggiornamento pannelli # -------------------------------------------------------------- panel_data = [] for panel in self.panels: data = panel.update( timestamp=timestamp, irradiance=irradiance, ambient_temperature=ambient_temperature, years_from_start=years_from_start, timestep_minutes=timestep_minutes ) panel_data.append(data) # -------------------------------------------------------------- # Aggregazione # -------------------------------------------------------------- power = self.calculate_power( panel_data ) voltage = self.calculate_voltage( panel_data ) current = self.calculate_current( panel_data ) # -------------------------------------------------------------- # Temperatura # -------------------------------------------------------------- internal_temperature = ( self.calculate_temperature( panel_data, ambient_temperature ) ) self.internal_temperature = ( internal_temperature ) # -------------------------------------------------------------- # Energia # -------------------------------------------------------------- energy = ( power * timestep_minutes / 60.0 ) self.total_energy_Wh += energy # -------------------------------------------------------------- # Output # -------------------------------------------------------------- return { "timestamp": timestamp, "combiner_id": self.combiner_id, "power_W": power, "current_A": current, "voltage_V": voltage, "energy_Wh": energy, "total_energy_Wh": self.total_energy_Wh, "active_panels": self.get_active_panel_count(), "total_panels": self.get_panel_count(), "internal_temperature_C": internal_temperature, "enabled": self.enabled } # ------------------------------------------------------------------ # Gestione guasti # ------------------------------------------------------------------ def fail(self) -> None: """ Mette la Combiner Box offline. Tutti i pannelli collegati continueranno a mantenere il proprio stato interno, ma la Combiner non trasferirà potenza verso l'inverter. """ self.enabled = False # ------------------------------------------------------------------ def repair(self) -> None: """ Riporta la Combiner Box in servizio. """ self.enabled = True # ------------------------------------------------------------------ def active_power(self) -> float: """ Restituisce una stima della potenza attiva corrente. Questo metodo è principalmente utile per diagnostica. """ if not self.enabled: return 0.0 return sum( panel.nominal_power for panel in self.panels if panel.enabled ) * self.cable_loss_factor # ------------------------------------------------------------------ def __repr__(self) -> str: """ Rappresentazione leggibile della Combiner Box. """ return ( f"CombinerBox(" f"id={self.combiner_id}, " f"panels={len(self.panels)}, " f"enabled={self.enabled})" ) ```