1135 lines
22 KiB
Python
1135 lines
22 KiB
Python
"""
|
|
faults.py
|
|
|
|
Motore di simulazione dei guasti e delle anomalie
|
|
per il simulatore di campo fotovoltaico.
|
|
|
|
Il modulo permette di simulare eventi a diversi livelli:
|
|
|
|
PVPanel
|
|
|
|
|
+-- degradation
|
|
+-- soiling
|
|
+-- partial_shading
|
|
+-- intermittent_fault
|
|
+-- permanent_fault
|
|
|
|
|
v
|
|
CombinerBox
|
|
|
|
|
+-- string_loss
|
|
+-- fuse_fault
|
|
+-- communication_fault
|
|
+-- complete_failure
|
|
|
|
|
v
|
|
Inverter
|
|
|
|
|
+-- derating
|
|
+-- overheating
|
|
+-- shutdown
|
|
+-- clipping
|
|
+-- communication_fault
|
|
|
|
Ogni fault produce un fattore di riduzione della produzione.
|
|
|
|
Esempio:
|
|
|
|
normal power = 1000 W
|
|
|
|
soiling factor = 0.90
|
|
shading factor = 0.80
|
|
|
|
effective power =
|
|
1000 * 0.90 * 0.80
|
|
|
|
= 720 W
|
|
|
|
Il modulo non modifica direttamente i componenti PVPanel,
|
|
CombinerBox o Inverter.
|
|
|
|
Fornisce invece un livello indipendente di simulazione
|
|
dei fault che può essere interrogato durante ogni timestep.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Dict, List, Optional
|
|
|
|
import random
|
|
|
|
|
|
# ======================================================================
|
|
# ENUMERAZIONE DEI TIPI DI FAULT
|
|
# ======================================================================
|
|
|
|
|
|
class FaultType(str, Enum):
|
|
"""
|
|
Tipologie di guasto disponibili.
|
|
"""
|
|
|
|
# --------------------------------------------------------------
|
|
# Pannello
|
|
# --------------------------------------------------------------
|
|
|
|
PANEL_DEGRADATION = (
|
|
"panel_degradation"
|
|
)
|
|
|
|
SOILING = (
|
|
"soiling"
|
|
)
|
|
|
|
PARTIAL_SHADING = (
|
|
"partial_shading"
|
|
)
|
|
|
|
PANEL_INTERMITTENT = (
|
|
"panel_intermittent"
|
|
)
|
|
|
|
PANEL_FAILURE = (
|
|
"panel_failure"
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Combiner
|
|
# --------------------------------------------------------------
|
|
|
|
COMBINER_STRING_LOSS = (
|
|
"combiner_string_loss"
|
|
)
|
|
|
|
COMBINER_FUSE_FAULT = (
|
|
"combiner_fuse_fault"
|
|
)
|
|
|
|
COMBINER_COMMUNICATION = (
|
|
"combiner_communication"
|
|
)
|
|
|
|
COMBINER_FAILURE = (
|
|
"combiner_failure"
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Inverter
|
|
# --------------------------------------------------------------
|
|
|
|
INVERTER_DERATING = (
|
|
"inverter_derating"
|
|
)
|
|
|
|
INVERTER_OVERHEATING = (
|
|
"inverter_overheating"
|
|
)
|
|
|
|
INVERTER_SHUTDOWN = (
|
|
"inverter_shutdown"
|
|
)
|
|
|
|
INVERTER_CLIPPING = (
|
|
"inverter_clipping"
|
|
)
|
|
|
|
INVERTER_COMMUNICATION = (
|
|
"inverter_communication"
|
|
)
|
|
|
|
|
|
# ======================================================================
|
|
# LIVELLO DEL COMPONENTE
|
|
# ======================================================================
|
|
|
|
|
|
class ComponentLevel(str, Enum):
|
|
"""
|
|
Livello gerarchico del componente affetto.
|
|
"""
|
|
|
|
PANEL = "panel"
|
|
|
|
COMBINER = "combiner"
|
|
|
|
INVERTER = "inverter"
|
|
|
|
|
|
# ======================================================================
|
|
# EVENTO DI FAULT
|
|
# ======================================================================
|
|
|
|
|
|
@dataclass
|
|
class FaultEvent:
|
|
"""
|
|
Rappresenta un singolo evento di guasto.
|
|
|
|
Parameters
|
|
----------
|
|
fault_id:
|
|
Identificativo univoco del fault.
|
|
|
|
fault_type:
|
|
Tipo di guasto.
|
|
|
|
component_level:
|
|
Livello gerarchico del componente.
|
|
|
|
component_id:
|
|
ID del componente interessato.
|
|
|
|
start_time:
|
|
Inizio del fault.
|
|
|
|
end_time:
|
|
Fine del fault.
|
|
None = fault permanente.
|
|
|
|
severity:
|
|
Severità del fault da 0.0 a 1.0.
|
|
|
|
enabled:
|
|
Indica se il fault è attivo.
|
|
|
|
description:
|
|
Descrizione opzionale.
|
|
"""
|
|
|
|
fault_id: str
|
|
|
|
fault_type: FaultType
|
|
|
|
component_level: ComponentLevel
|
|
|
|
component_id: str
|
|
|
|
start_time: datetime
|
|
|
|
end_time: Optional[datetime] = None
|
|
|
|
severity: float = 1.0
|
|
|
|
enabled: bool = True
|
|
|
|
description: str = ""
|
|
|
|
# --------------------------------------------------------------
|
|
# Validazione
|
|
# --------------------------------------------------------------
|
|
|
|
def __post_init__(
|
|
self
|
|
) -> None:
|
|
|
|
self.severity = min(
|
|
|
|
max(
|
|
|
|
self.severity,
|
|
|
|
0.0
|
|
),
|
|
|
|
1.0
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Stato
|
|
# --------------------------------------------------------------
|
|
|
|
def is_active(
|
|
self,
|
|
timestamp: datetime
|
|
) -> bool:
|
|
"""
|
|
Verifica se il fault è attivo al timestamp indicato.
|
|
"""
|
|
|
|
if not self.enabled:
|
|
|
|
return False
|
|
|
|
if timestamp < self.start_time:
|
|
|
|
return False
|
|
|
|
if (
|
|
self.end_time is not None
|
|
and timestamp >= self.end_time
|
|
):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
# --------------------------------------------------------------
|
|
# Fattore di riduzione
|
|
# --------------------------------------------------------------
|
|
|
|
def reduction_factor(
|
|
self
|
|
) -> float:
|
|
"""
|
|
Restituisce il fattore di produzione associato
|
|
alla severità del fault.
|
|
|
|
severity = 0.0
|
|
-> nessuna perdita
|
|
|
|
severity = 1.0
|
|
-> perdita completa
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
Fattore compreso tra 0.0 e 1.0.
|
|
"""
|
|
|
|
return 1.0 - self.severity
|
|
|
|
|
|
# ======================================================================
|
|
# CONFIGURAZIONE FAULT
|
|
# ======================================================================
|
|
|
|
|
|
@dataclass
|
|
class FaultConfig:
|
|
"""
|
|
Configurazione per la generazione automatica dei fault.
|
|
"""
|
|
|
|
enabled: bool = True
|
|
|
|
panel_fault_probability: float = 0.0001
|
|
|
|
combiner_fault_probability: float = 0.00002
|
|
|
|
inverter_fault_probability: float = 0.00001
|
|
|
|
intermittent_probability: float = 0.30
|
|
|
|
default_fault_duration_hours: float = 4.0
|
|
|
|
random_seed: Optional[int] = None
|
|
|
|
|
|
# ======================================================================
|
|
# FAULT MANAGER
|
|
# ======================================================================
|
|
|
|
|
|
@dataclass
|
|
class FaultManager:
|
|
"""
|
|
Gestore centrale dei fault dell'impianto.
|
|
|
|
Mantiene tutti gli eventi attivi e storici.
|
|
|
|
Il FaultManager può:
|
|
|
|
- aggiungere fault manualmente;
|
|
- rimuovere fault;
|
|
- verificare i fault attivi;
|
|
- calcolare il fattore di produzione;
|
|
- generare fault casuali;
|
|
- fornire informazioni diagnostiche.
|
|
"""
|
|
|
|
config: FaultConfig = field(
|
|
default_factory=FaultConfig
|
|
)
|
|
|
|
faults: List[FaultEvent] = field(
|
|
default_factory=list
|
|
)
|
|
|
|
_fault_counter: int = 0
|
|
|
|
# ------------------------------------------------------------------
|
|
# Inizializzazione
|
|
# ------------------------------------------------------------------
|
|
|
|
def __post_init__(
|
|
self
|
|
) -> None:
|
|
|
|
if (
|
|
self.config.random_seed
|
|
is not None
|
|
):
|
|
|
|
random.seed(
|
|
self.config.random_seed
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Generazione ID
|
|
# ------------------------------------------------------------------
|
|
|
|
def _generate_fault_id(
|
|
self
|
|
) -> str:
|
|
|
|
self._fault_counter += 1
|
|
|
|
return (
|
|
|
|
f"FAULT_"
|
|
f"{self._fault_counter:06d}"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Aggiunta fault
|
|
# ------------------------------------------------------------------
|
|
|
|
def add_fault(
|
|
self,
|
|
fault_type: FaultType,
|
|
component_level: ComponentLevel,
|
|
component_id: str,
|
|
start_time: datetime,
|
|
severity: float = 1.0,
|
|
duration_hours: Optional[float] = None,
|
|
description: str = ""
|
|
) -> FaultEvent:
|
|
"""
|
|
Crea e registra un nuovo fault.
|
|
"""
|
|
|
|
end_time = None
|
|
|
|
if duration_hours is not None:
|
|
|
|
from datetime import timedelta
|
|
|
|
end_time = (
|
|
|
|
start_time
|
|
|
|
+ timedelta(
|
|
|
|
hours=
|
|
duration_hours
|
|
)
|
|
)
|
|
|
|
fault = FaultEvent(
|
|
|
|
fault_id=
|
|
self._generate_fault_id(),
|
|
|
|
fault_type=
|
|
fault_type,
|
|
|
|
component_level=
|
|
component_level,
|
|
|
|
component_id=
|
|
component_id,
|
|
|
|
start_time=
|
|
start_time,
|
|
|
|
end_time=
|
|
end_time,
|
|
|
|
severity=
|
|
severity,
|
|
|
|
description=
|
|
description
|
|
)
|
|
|
|
self.faults.append(
|
|
fault
|
|
)
|
|
|
|
return fault
|
|
|
|
# ------------------------------------------------------------------
|
|
# Rimozione fault
|
|
# ------------------------------------------------------------------
|
|
|
|
def remove_fault(
|
|
self,
|
|
fault_id: str
|
|
) -> bool:
|
|
"""
|
|
Rimuove un fault tramite ID.
|
|
"""
|
|
|
|
for fault in self.faults:
|
|
|
|
if (
|
|
fault.fault_id
|
|
== fault_id
|
|
):
|
|
|
|
self.faults.remove(
|
|
fault
|
|
)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
# ------------------------------------------------------------------
|
|
# Disabilitazione fault
|
|
# ------------------------------------------------------------------
|
|
|
|
def disable_fault(
|
|
self,
|
|
fault_id: str
|
|
) -> bool:
|
|
"""
|
|
Disabilita un fault senza eliminarlo dallo storico.
|
|
"""
|
|
|
|
for fault in self.faults:
|
|
|
|
if (
|
|
fault.fault_id
|
|
== fault_id
|
|
):
|
|
|
|
fault.enabled = False
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
# ------------------------------------------------------------------
|
|
# Fault attivi
|
|
# ------------------------------------------------------------------
|
|
|
|
def get_active_faults(
|
|
self,
|
|
timestamp: datetime
|
|
) -> List[FaultEvent]:
|
|
"""
|
|
Restituisce tutti i fault attivi.
|
|
"""
|
|
|
|
return [
|
|
|
|
fault
|
|
|
|
for fault in self.faults
|
|
|
|
if fault.is_active(
|
|
timestamp
|
|
)
|
|
]
|
|
|
|
# ------------------------------------------------------------------
|
|
# Fault di un componente
|
|
# ------------------------------------------------------------------
|
|
|
|
def get_component_faults(
|
|
self,
|
|
component_id: str,
|
|
timestamp: datetime
|
|
) -> List[FaultEvent]:
|
|
"""
|
|
Restituisce i fault attivi associati
|
|
a uno specifico componente.
|
|
"""
|
|
|
|
return [
|
|
|
|
fault
|
|
|
|
for fault in self.faults
|
|
|
|
if (
|
|
|
|
fault.component_id
|
|
== component_id
|
|
|
|
and fault.is_active(
|
|
timestamp
|
|
)
|
|
)
|
|
]
|
|
|
|
# ------------------------------------------------------------------
|
|
# Fattore componente
|
|
# ------------------------------------------------------------------
|
|
|
|
def get_component_factor(
|
|
self,
|
|
component_id: str,
|
|
timestamp: datetime
|
|
) -> float:
|
|
"""
|
|
Calcola il fattore di produzione complessivo
|
|
di un componente.
|
|
|
|
Se sono presenti più fault contemporaneamente,
|
|
i fattori vengono moltiplicati.
|
|
|
|
Esempio:
|
|
|
|
Soiling = 0.90
|
|
Shading = 0.80
|
|
|
|
Factor =
|
|
0.90 * 0.80
|
|
|
|
= 0.72
|
|
"""
|
|
|
|
active_faults = (
|
|
self.get_component_faults(
|
|
|
|
component_id,
|
|
|
|
timestamp
|
|
)
|
|
)
|
|
|
|
factor = 1.0
|
|
|
|
for fault in active_faults:
|
|
|
|
factor *= (
|
|
|
|
fault.reduction_factor()
|
|
)
|
|
|
|
return min(
|
|
|
|
max(
|
|
|
|
factor,
|
|
|
|
0.0
|
|
),
|
|
|
|
1.0
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Stato componente
|
|
# ------------------------------------------------------------------
|
|
|
|
def get_component_status(
|
|
self,
|
|
component_id: str,
|
|
timestamp: datetime
|
|
) -> Dict[str, object]:
|
|
"""
|
|
Restituisce lo stato diagnostico di un componente.
|
|
"""
|
|
|
|
faults = (
|
|
self.get_component_faults(
|
|
|
|
component_id,
|
|
|
|
timestamp
|
|
)
|
|
)
|
|
|
|
factor = (
|
|
self.get_component_factor(
|
|
|
|
component_id,
|
|
|
|
timestamp
|
|
)
|
|
)
|
|
|
|
return {
|
|
|
|
"component_id":
|
|
component_id,
|
|
|
|
"healthy":
|
|
len(
|
|
faults
|
|
) == 0,
|
|
|
|
"fault_count":
|
|
len(
|
|
faults
|
|
),
|
|
|
|
"production_factor":
|
|
factor,
|
|
|
|
"faults":
|
|
faults
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Generazione fault casuale
|
|
# ------------------------------------------------------------------
|
|
|
|
def generate_random_fault(
|
|
self,
|
|
component_id: str,
|
|
component_level: ComponentLevel,
|
|
timestamp: datetime
|
|
) -> Optional[FaultEvent]:
|
|
"""
|
|
Genera casualmente un fault.
|
|
|
|
Il metodo è pensato per essere chiamato
|
|
durante ogni timestep della simulazione.
|
|
|
|
La probabilità viene scelta in base
|
|
al livello gerarchico.
|
|
"""
|
|
|
|
if not self.config.enabled:
|
|
|
|
return None
|
|
|
|
if (
|
|
component_level
|
|
== ComponentLevel.PANEL
|
|
):
|
|
|
|
probability = (
|
|
|
|
self.config
|
|
.panel_fault_probability
|
|
)
|
|
|
|
elif (
|
|
component_level
|
|
== ComponentLevel.COMBINER
|
|
):
|
|
|
|
probability = (
|
|
|
|
self.config
|
|
.combiner_fault_probability
|
|
)
|
|
|
|
else:
|
|
|
|
probability = (
|
|
|
|
self.config
|
|
.inverter_fault_probability
|
|
)
|
|
|
|
# Nessun fault
|
|
if (
|
|
random.random()
|
|
>= probability
|
|
):
|
|
|
|
return None
|
|
|
|
# --------------------------------------------------------------
|
|
# Selezione tipologia
|
|
# --------------------------------------------------------------
|
|
|
|
if (
|
|
component_level
|
|
== ComponentLevel.PANEL
|
|
):
|
|
|
|
fault_type = random.choice([
|
|
|
|
FaultType.SOILING,
|
|
|
|
FaultType.PARTIAL_SHADING,
|
|
|
|
FaultType.PANEL_INTERMITTENT,
|
|
|
|
FaultType.PANEL_FAILURE
|
|
])
|
|
|
|
elif (
|
|
component_level
|
|
== ComponentLevel.COMBINER
|
|
):
|
|
|
|
fault_type = random.choice([
|
|
|
|
FaultType.COMBINER_STRING_LOSS,
|
|
|
|
FaultType.COMBINER_FUSE_FAULT,
|
|
|
|
FaultType.COMBINER_COMMUNICATION,
|
|
|
|
FaultType.COMBINER_FAILURE
|
|
])
|
|
|
|
else:
|
|
|
|
fault_type = random.choice([
|
|
|
|
FaultType.INVERTER_DERATING,
|
|
|
|
FaultType.INVERTER_OVERHEATING,
|
|
|
|
FaultType.INVERTER_SHUTDOWN,
|
|
|
|
FaultType.INVERTER_CLIPPING,
|
|
|
|
FaultType.INVERTER_COMMUNICATION
|
|
])
|
|
|
|
# --------------------------------------------------------------
|
|
# Intermittent fault
|
|
# --------------------------------------------------------------
|
|
|
|
is_intermittent = (
|
|
|
|
random.random()
|
|
|
|
< self.config
|
|
.intermittent_probability
|
|
)
|
|
|
|
if is_intermittent:
|
|
|
|
duration = (
|
|
|
|
random.uniform(
|
|
|
|
0.5,
|
|
|
|
self.config
|
|
.default_fault_duration_hours
|
|
)
|
|
)
|
|
|
|
else:
|
|
|
|
duration = None
|
|
|
|
# --------------------------------------------------------------
|
|
# Severità
|
|
# --------------------------------------------------------------
|
|
|
|
if (
|
|
fault_type
|
|
in [
|
|
|
|
FaultType.PANEL_FAILURE,
|
|
|
|
FaultType.COMBINER_FAILURE,
|
|
|
|
FaultType.INVERTER_SHUTDOWN
|
|
]
|
|
):
|
|
|
|
severity = 1.0
|
|
|
|
elif (
|
|
fault_type
|
|
== FaultType.SOILING
|
|
):
|
|
|
|
severity = random.uniform(
|
|
|
|
0.05,
|
|
|
|
0.30
|
|
)
|
|
|
|
elif (
|
|
fault_type
|
|
== FaultType.PARTIAL_SHADING
|
|
):
|
|
|
|
severity = random.uniform(
|
|
|
|
0.10,
|
|
|
|
0.70
|
|
)
|
|
|
|
elif (
|
|
fault_type
|
|
== FaultType.INVERTER_DERATING
|
|
):
|
|
|
|
severity = random.uniform(
|
|
|
|
0.10,
|
|
|
|
0.50
|
|
)
|
|
|
|
else:
|
|
|
|
severity = random.uniform(
|
|
|
|
0.10,
|
|
|
|
0.40
|
|
)
|
|
|
|
return self.add_fault(
|
|
|
|
fault_type=
|
|
fault_type,
|
|
|
|
component_level=
|
|
component_level,
|
|
|
|
component_id=
|
|
component_id,
|
|
|
|
start_time=
|
|
timestamp,
|
|
|
|
severity=
|
|
severity,
|
|
|
|
duration_hours=
|
|
duration,
|
|
|
|
description=
|
|
"Automatically generated fault"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Simulazione automatica
|
|
# ------------------------------------------------------------------
|
|
|
|
def simulate_random_faults(
|
|
self,
|
|
timestamp: datetime,
|
|
panel_ids: List[str],
|
|
combiner_ids: List[str],
|
|
inverter_ids: List[str]
|
|
) -> List[FaultEvent]:
|
|
"""
|
|
Esegue la generazione casuale dei fault
|
|
per tutti i componenti dell'impianto.
|
|
|
|
Returns
|
|
-------
|
|
List[FaultEvent]
|
|
Fault generati durante il timestep.
|
|
"""
|
|
|
|
generated = []
|
|
|
|
for panel_id in panel_ids:
|
|
|
|
fault = (
|
|
self.generate_random_fault(
|
|
|
|
component_id=
|
|
panel_id,
|
|
|
|
component_level=
|
|
ComponentLevel.PANEL,
|
|
|
|
timestamp=
|
|
timestamp
|
|
)
|
|
)
|
|
|
|
if fault is not None:
|
|
|
|
generated.append(
|
|
fault
|
|
)
|
|
|
|
for combiner_id in combiner_ids:
|
|
|
|
fault = (
|
|
self.generate_random_fault(
|
|
|
|
component_id=
|
|
combiner_id,
|
|
|
|
component_level=
|
|
ComponentLevel.COMBINER,
|
|
|
|
timestamp=
|
|
timestamp
|
|
)
|
|
)
|
|
|
|
if fault is not None:
|
|
|
|
generated.append(
|
|
fault
|
|
)
|
|
|
|
for inverter_id in inverter_ids:
|
|
|
|
fault = (
|
|
self.generate_random_fault(
|
|
|
|
component_id=
|
|
inverter_id,
|
|
|
|
component_level=
|
|
ComponentLevel.INVERTER,
|
|
|
|
timestamp=
|
|
timestamp
|
|
)
|
|
)
|
|
|
|
if fault is not None:
|
|
|
|
generated.append(
|
|
fault
|
|
)
|
|
|
|
return generated
|
|
|
|
# ------------------------------------------------------------------
|
|
# Diagnostica
|
|
# ------------------------------------------------------------------
|
|
|
|
def diagnostics(
|
|
self,
|
|
timestamp: datetime
|
|
) -> Dict[str, object]:
|
|
"""
|
|
Restituisce una panoramica diagnostica
|
|
dei fault attivi.
|
|
"""
|
|
|
|
active = (
|
|
self.get_active_faults(
|
|
timestamp
|
|
)
|
|
)
|
|
|
|
by_level = {
|
|
|
|
"panel": 0,
|
|
|
|
"combiner": 0,
|
|
|
|
"inverter": 0
|
|
}
|
|
|
|
by_type = {}
|
|
|
|
for fault in active:
|
|
|
|
by_level[
|
|
fault.component_level.value
|
|
] += 1
|
|
|
|
fault_name = (
|
|
fault.fault_type.value
|
|
)
|
|
|
|
by_type[
|
|
fault_name
|
|
] = (
|
|
|
|
by_type.get(
|
|
fault_name,
|
|
0
|
|
)
|
|
|
|
+ 1
|
|
)
|
|
|
|
return {
|
|
|
|
"timestamp":
|
|
timestamp,
|
|
|
|
"active_faults":
|
|
len(
|
|
active
|
|
),
|
|
|
|
"by_level":
|
|
by_level,
|
|
|
|
"by_type":
|
|
by_type,
|
|
|
|
"faults":
|
|
active
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Pulizia fault scaduti
|
|
# ------------------------------------------------------------------
|
|
|
|
def cleanup_expired_faults(
|
|
self,
|
|
timestamp: datetime
|
|
) -> int:
|
|
"""
|
|
Rimuove i fault terminati.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
Numero di fault rimossi.
|
|
"""
|
|
|
|
before = len(
|
|
self.faults
|
|
)
|
|
|
|
self.faults = [
|
|
|
|
fault
|
|
|
|
for fault in self.faults
|
|
|
|
if (
|
|
|
|
fault.end_time is None
|
|
|
|
or fault.end_time > timestamp
|
|
)
|
|
]
|
|
|
|
return (
|
|
|
|
before
|
|
- len(
|
|
self.faults
|
|
)
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Rappresentazione
|
|
# ------------------------------------------------------------------
|
|
|
|
def __repr__(
|
|
self
|
|
) -> str:
|
|
"""
|
|
Rappresentazione leggibile.
|
|
"""
|
|
|
|
active = sum(
|
|
|
|
1
|
|
|
|
for fault
|
|
in self.faults
|
|
|
|
if fault.enabled
|
|
)
|
|
|
|
return (
|
|
|
|
f"FaultManager("
|
|
f"total_faults="
|
|
f"{len(self.faults)}, "
|
|
f"active="
|
|
f"{active})"
|
|
)
|
|
```
|