483 lines
11 KiB
Python
483 lines
11 KiB
Python
```python
|
|
"""
|
|
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})"
|
|
)
|
|
```
|