1445 lines
26 KiB
Python
1445 lines
26 KiB
Python
"""
|
|
pvsim/storage.py
|
|
|
|
Layer di persistenza dei dati prodotti dal simulatore fotovoltaico.
|
|
|
|
Il modulo permette di salvare SimulationResult in:
|
|
|
|
- CSV
|
|
- Parquet
|
|
- SQLite
|
|
|
|
Struttura logica dei dati:
|
|
|
|
simulation
|
|
|
|
|
+-- weather
|
|
|
|
|
+-- panel
|
|
|
|
|
+-- combiner
|
|
|
|
|
+-- inverter
|
|
|
|
|
+-- plant
|
|
|
|
|
+-- faults
|
|
|
|
Il modulo è indipendente dal motore di simulazione.
|
|
|
|
Pipeline:
|
|
|
|
PVSimulator
|
|
|
|
|
v
|
|
SimulationResult
|
|
|
|
|
v
|
|
SimulationStorage
|
|
|
|
|
+---- CSV
|
|
|
|
|
+---- Parquet
|
|
|
|
|
+---- SQLite
|
|
|
|
|
|
Esempio:
|
|
|
|
result = simulator.run(config)
|
|
|
|
storage = SimulationStorage(
|
|
output_dir="output"
|
|
)
|
|
|
|
storage.save_csv(result)
|
|
|
|
storage.save_parquet(result)
|
|
|
|
storage.save_sqlite(
|
|
result,
|
|
"output/pv_simulation.db"
|
|
)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import Dict, Optional
|
|
|
|
import pandas as pd
|
|
|
|
from .simulator import SimulationResult
|
|
|
|
|
|
# ======================================================================
|
|
# STORAGE CONFIGURATION
|
|
# ======================================================================
|
|
|
|
|
|
@dataclass
|
|
class StorageConfig:
|
|
"""
|
|
Configurazione del sistema di storage.
|
|
|
|
Parameters
|
|
----------
|
|
output_dir:
|
|
Directory principale di output.
|
|
|
|
csv_dir:
|
|
Sottodirectory CSV.
|
|
|
|
parquet_dir:
|
|
Sottodirectory Parquet.
|
|
|
|
sqlite_filename:
|
|
Nome del database SQLite.
|
|
|
|
overwrite:
|
|
Se True, i file esistenti vengono sovrascritti.
|
|
|
|
compression:
|
|
Compressione Parquet.
|
|
|
|
Valori tipici:
|
|
|
|
"snappy"
|
|
"gzip"
|
|
"brotli"
|
|
None
|
|
"""
|
|
|
|
output_dir: str = "output"
|
|
|
|
csv_dir: str = "csv"
|
|
|
|
parquet_dir: str = "parquet"
|
|
|
|
sqlite_filename: str = "pv_simulation.db"
|
|
|
|
overwrite: bool = True
|
|
|
|
compression: Optional[str] = "snappy"
|
|
|
|
def output_path(
|
|
self,
|
|
) -> Path:
|
|
"""
|
|
Restituisce il path principale di output.
|
|
"""
|
|
|
|
return Path(
|
|
|
|
self.output_dir
|
|
)
|
|
|
|
def csv_path(
|
|
self,
|
|
) -> Path:
|
|
"""
|
|
Restituisce il path della directory CSV.
|
|
"""
|
|
|
|
return (
|
|
|
|
self.output_path()
|
|
|
|
/ self.csv_dir
|
|
)
|
|
|
|
def parquet_path(
|
|
self,
|
|
) -> Path:
|
|
"""
|
|
Restituisce il path della directory Parquet.
|
|
"""
|
|
|
|
return (
|
|
|
|
self.output_path()
|
|
|
|
/ self.parquet_dir
|
|
)
|
|
|
|
def sqlite_path(
|
|
self,
|
|
) -> Path:
|
|
"""
|
|
Restituisce il path del database SQLite.
|
|
"""
|
|
|
|
return (
|
|
|
|
self.output_path()
|
|
|
|
/ self.sqlite_filename
|
|
)
|
|
|
|
|
|
# ======================================================================
|
|
# STORAGE CLASS
|
|
# ======================================================================
|
|
|
|
|
|
class SimulationStorage:
|
|
"""
|
|
Gestisce la persistenza dei risultati della simulazione.
|
|
|
|
Parameters
|
|
----------
|
|
config:
|
|
Configurazione dello storage.
|
|
|
|
Esempio
|
|
-------
|
|
|
|
storage = SimulationStorage()
|
|
|
|
storage.save_csv(
|
|
result
|
|
)
|
|
|
|
storage.save_parquet(
|
|
result
|
|
)
|
|
|
|
storage.save_sqlite(
|
|
result
|
|
)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
config: Optional[
|
|
StorageConfig
|
|
] = None,
|
|
output_dir: Optional[
|
|
str
|
|
] = None,
|
|
) -> None:
|
|
|
|
if config is not None:
|
|
|
|
self.config = config
|
|
|
|
elif output_dir is not None:
|
|
|
|
self.config = StorageConfig(
|
|
|
|
output_dir=
|
|
|
|
output_dir
|
|
)
|
|
|
|
else:
|
|
|
|
self.config = StorageConfig()
|
|
|
|
self._create_directories()
|
|
|
|
# ==================================================================
|
|
# DIRECTORIES
|
|
# ==================================================================
|
|
|
|
def _create_directories(
|
|
self,
|
|
) -> None:
|
|
"""
|
|
Crea le directory necessarie.
|
|
"""
|
|
|
|
self.config.output_path().mkdir(
|
|
|
|
parents=True,
|
|
|
|
exist_ok=True
|
|
)
|
|
|
|
self.config.csv_path().mkdir(
|
|
|
|
parents=True,
|
|
|
|
exist_ok=True
|
|
)
|
|
|
|
self.config.parquet_path().mkdir(
|
|
|
|
parents=True,
|
|
|
|
exist_ok=True
|
|
)
|
|
|
|
# ==================================================================
|
|
# DATASET EXTRACTION
|
|
# ==================================================================
|
|
|
|
def _get_datasets(
|
|
self,
|
|
result: SimulationResult,
|
|
) -> Dict[
|
|
str,
|
|
pd.DataFrame
|
|
]:
|
|
"""
|
|
Estrae tutti i dataset dal SimulationResult.
|
|
|
|
Returns
|
|
-------
|
|
Dict[str, DataFrame]
|
|
|
|
Keys:
|
|
|
|
weather
|
|
panel
|
|
combiner
|
|
inverter
|
|
plant
|
|
faults
|
|
"""
|
|
|
|
return {
|
|
|
|
"weather":
|
|
result.weather_dataframe(),
|
|
|
|
"panel":
|
|
result.panel_dataframe(),
|
|
|
|
"combiner":
|
|
result.combiner_dataframe(),
|
|
|
|
"inverter":
|
|
result.inverter_dataframe(),
|
|
|
|
"plant":
|
|
result.plant_dataframe(),
|
|
|
|
"faults":
|
|
result.faults_dataframe(),
|
|
}
|
|
|
|
# ==================================================================
|
|
# DATAFRAME NORMALIZATION
|
|
# ==================================================================
|
|
|
|
@staticmethod
|
|
def _normalize_dataframe(
|
|
dataframe: pd.DataFrame,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Normalizza un DataFrame prima del salvataggio.
|
|
|
|
In particolare:
|
|
|
|
- converte timestamp in datetime;
|
|
- ordina temporalmente;
|
|
- resetta l'indice.
|
|
"""
|
|
|
|
if dataframe.empty:
|
|
|
|
return dataframe
|
|
|
|
dataframe = dataframe.copy()
|
|
|
|
if "timestamp" in dataframe.columns:
|
|
|
|
dataframe[
|
|
"timestamp"
|
|
] = pd.to_datetime(
|
|
|
|
dataframe[
|
|
"timestamp"
|
|
]
|
|
)
|
|
|
|
dataframe = dataframe.sort_values(
|
|
|
|
by="timestamp"
|
|
)
|
|
|
|
dataframe = dataframe.reset_index(
|
|
|
|
drop=True
|
|
)
|
|
|
|
return dataframe
|
|
|
|
# ==================================================================
|
|
# CSV
|
|
# ==================================================================
|
|
|
|
def save_csv(
|
|
self,
|
|
result: SimulationResult,
|
|
prefix: str = "",
|
|
) -> Dict[
|
|
str,
|
|
Path
|
|
]:
|
|
"""
|
|
Salva i dati in formato CSV.
|
|
|
|
Crea:
|
|
|
|
weather.csv
|
|
panel.csv
|
|
combiner.csv
|
|
inverter.csv
|
|
plant.csv
|
|
faults.csv
|
|
|
|
Parameters
|
|
----------
|
|
result:
|
|
Risultato della simulazione.
|
|
|
|
prefix:
|
|
Prefisso opzionale per i file.
|
|
|
|
Returns
|
|
-------
|
|
Dict[str, Path]
|
|
Path dei file creati.
|
|
"""
|
|
|
|
datasets = (
|
|
|
|
self._get_datasets(
|
|
|
|
result
|
|
)
|
|
)
|
|
|
|
created_files = {}
|
|
|
|
for name, dataframe in datasets.items():
|
|
|
|
dataframe = (
|
|
|
|
self._normalize_dataframe(
|
|
|
|
dataframe
|
|
)
|
|
)
|
|
|
|
if dataframe.empty:
|
|
|
|
continue
|
|
|
|
filename = (
|
|
|
|
f"{prefix}{name}.csv"
|
|
)
|
|
|
|
output_file = (
|
|
|
|
self.config.csv_path()
|
|
|
|
/ filename
|
|
)
|
|
|
|
if (
|
|
|
|
output_file.exists()
|
|
|
|
and not self.config.overwrite
|
|
):
|
|
|
|
raise FileExistsError(
|
|
|
|
f"Il file esiste già: "
|
|
f"{output_file}"
|
|
)
|
|
|
|
dataframe.to_csv(
|
|
|
|
output_file,
|
|
|
|
index=False
|
|
)
|
|
|
|
created_files[
|
|
name
|
|
] = output_file
|
|
|
|
return created_files
|
|
|
|
# ==================================================================
|
|
# PARQUET
|
|
# ==================================================================
|
|
|
|
def save_parquet(
|
|
self,
|
|
result: SimulationResult,
|
|
prefix: str = "",
|
|
) -> Dict[
|
|
str,
|
|
Path
|
|
]:
|
|
"""
|
|
Salva i dati in formato Parquet.
|
|
|
|
Parquet è consigliato per simulazioni
|
|
di grandi dimensioni perché:
|
|
|
|
- occupa meno spazio;
|
|
- è più veloce da leggere;
|
|
- mantiene i tipi delle colonne;
|
|
- è adatto a Data Lake e pipeline analytics.
|
|
|
|
Returns
|
|
-------
|
|
Dict[str, Path]
|
|
Path dei file creati.
|
|
"""
|
|
|
|
datasets = (
|
|
|
|
self._get_datasets(
|
|
|
|
result
|
|
)
|
|
)
|
|
|
|
created_files = {}
|
|
|
|
for name, dataframe in datasets.items():
|
|
|
|
dataframe = (
|
|
|
|
self._normalize_dataframe(
|
|
|
|
dataframe
|
|
)
|
|
)
|
|
|
|
if dataframe.empty:
|
|
|
|
continue
|
|
|
|
filename = (
|
|
|
|
f"{prefix}{name}.parquet"
|
|
)
|
|
|
|
output_file = (
|
|
|
|
self.config.parquet_path()
|
|
|
|
/ filename
|
|
)
|
|
|
|
if (
|
|
|
|
output_file.exists()
|
|
|
|
and not self.config.overwrite
|
|
):
|
|
|
|
raise FileExistsError(
|
|
|
|
f"Il file esiste già: "
|
|
f"{output_file}"
|
|
)
|
|
|
|
dataframe.to_parquet(
|
|
|
|
output_file,
|
|
|
|
index=False,
|
|
|
|
compression=
|
|
|
|
self.config.compression
|
|
)
|
|
|
|
created_files[
|
|
name
|
|
] = output_file
|
|
|
|
return created_files
|
|
|
|
# ==================================================================
|
|
# SQLITE CONNECTION
|
|
# ==================================================================
|
|
|
|
def _connect_sqlite(
|
|
self,
|
|
database_path: Optional[
|
|
str
|
|
] = None,
|
|
) -> sqlite3.Connection:
|
|
"""
|
|
Apre una connessione SQLite.
|
|
"""
|
|
|
|
if database_path is None:
|
|
|
|
database_path = (
|
|
|
|
str(
|
|
|
|
self.config
|
|
.sqlite_path()
|
|
)
|
|
)
|
|
|
|
else:
|
|
|
|
database_path = str(
|
|
|
|
database_path
|
|
)
|
|
|
|
Path(
|
|
|
|
database_path
|
|
).parent.mkdir(
|
|
|
|
parents=True,
|
|
|
|
exist_ok=True
|
|
)
|
|
|
|
connection = sqlite3.connect(
|
|
|
|
database_path
|
|
)
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
PRAGMA journal_mode=WAL;
|
|
"""
|
|
)
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
PRAGMA foreign_keys=ON;
|
|
"""
|
|
)
|
|
|
|
return connection
|
|
|
|
# ==================================================================
|
|
# SQLITE SCHEMA
|
|
# ==================================================================
|
|
|
|
def _create_sqlite_schema(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
) -> None:
|
|
"""
|
|
Crea gli indici principali del database.
|
|
|
|
Le tabelle vengono create dinamicamente
|
|
da pandas.to_sql.
|
|
|
|
Gli indici vengono poi aggiunti manualmente.
|
|
"""
|
|
|
|
# --------------------------------------------------------------
|
|
# Panel
|
|
# --------------------------------------------------------------
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_panel_timestamp
|
|
|
|
ON panel(timestamp);
|
|
"""
|
|
)
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_panel_id_timestamp
|
|
|
|
ON panel(
|
|
panel_id,
|
|
timestamp
|
|
);
|
|
"""
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Combiner
|
|
# --------------------------------------------------------------
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_combiner_timestamp
|
|
|
|
ON combiner(timestamp);
|
|
"""
|
|
)
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_combiner_id_timestamp
|
|
|
|
ON combiner(
|
|
combiner_id,
|
|
timestamp
|
|
);
|
|
"""
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Inverter
|
|
# --------------------------------------------------------------
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_inverter_timestamp
|
|
|
|
ON inverter(timestamp);
|
|
"""
|
|
)
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_inverter_id_timestamp
|
|
|
|
ON inverter(
|
|
inverter_id,
|
|
timestamp
|
|
);
|
|
"""
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Plant
|
|
# --------------------------------------------------------------
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_plant_timestamp
|
|
|
|
ON plant(timestamp);
|
|
"""
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Weather
|
|
# --------------------------------------------------------------
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_weather_timestamp
|
|
|
|
ON weather(timestamp);
|
|
"""
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Faults
|
|
# --------------------------------------------------------------
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_fault_timestamp
|
|
|
|
ON faults(timestamp);
|
|
"""
|
|
)
|
|
|
|
connection.execute(
|
|
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS
|
|
idx_fault_component
|
|
|
|
ON faults(
|
|
component_id,
|
|
timestamp
|
|
);
|
|
"""
|
|
)
|
|
|
|
connection.commit()
|
|
|
|
# ==================================================================
|
|
# SQLITE
|
|
# ==================================================================
|
|
|
|
def save_sqlite(
|
|
self,
|
|
result: SimulationResult,
|
|
database_path: Optional[
|
|
str
|
|
] = None,
|
|
if_exists: str = "append",
|
|
) -> Path:
|
|
"""
|
|
Salva il risultato in SQLite.
|
|
|
|
Tabelle create:
|
|
|
|
weather
|
|
panel
|
|
combiner
|
|
inverter
|
|
plant
|
|
faults
|
|
|
|
Parameters
|
|
----------
|
|
result:
|
|
Risultato della simulazione.
|
|
|
|
database_path:
|
|
Path opzionale del database.
|
|
|
|
if_exists:
|
|
Strategia pandas.to_sql:
|
|
|
|
append
|
|
replace
|
|
fail
|
|
|
|
Returns
|
|
-------
|
|
Path
|
|
Path del database.
|
|
"""
|
|
|
|
if database_path is None:
|
|
|
|
database_path = (
|
|
|
|
self.config.sqlite_path()
|
|
)
|
|
|
|
database_path = Path(
|
|
|
|
database_path
|
|
)
|
|
|
|
if (
|
|
|
|
database_path.exists()
|
|
|
|
and not self.config.overwrite
|
|
|
|
and if_exists == "replace"
|
|
):
|
|
|
|
raise FileExistsError(
|
|
|
|
f"Il database esiste già: "
|
|
f"{database_path}"
|
|
)
|
|
|
|
connection = (
|
|
|
|
self._connect_sqlite(
|
|
|
|
str(
|
|
|
|
database_path
|
|
)
|
|
)
|
|
)
|
|
|
|
try:
|
|
|
|
datasets = (
|
|
|
|
self._get_datasets(
|
|
|
|
result
|
|
)
|
|
)
|
|
|
|
for name, dataframe in datasets.items():
|
|
|
|
dataframe = (
|
|
|
|
self._normalize_dataframe(
|
|
|
|
dataframe
|
|
)
|
|
)
|
|
|
|
if dataframe.empty:
|
|
|
|
continue
|
|
|
|
dataframe.to_sql(
|
|
|
|
name,
|
|
|
|
connection,
|
|
|
|
if_exists=
|
|
|
|
if_exists,
|
|
|
|
index=False
|
|
)
|
|
|
|
self._create_sqlite_schema(
|
|
|
|
connection
|
|
)
|
|
|
|
finally:
|
|
|
|
connection.close()
|
|
|
|
return database_path
|
|
|
|
# ==================================================================
|
|
# QUERY SQLITE
|
|
# ==================================================================
|
|
|
|
def query_sqlite(
|
|
self,
|
|
query: str,
|
|
database_path: Optional[
|
|
str
|
|
] = None,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Esegue una query SQL e restituisce un DataFrame.
|
|
|
|
Esempio:
|
|
|
|
df = storage.query_sqlite(
|
|
'''
|
|
SELECT
|
|
timestamp,
|
|
ac_power_W
|
|
FROM plant
|
|
ORDER BY timestamp
|
|
'''
|
|
)
|
|
"""
|
|
|
|
if database_path is None:
|
|
|
|
database_path = (
|
|
|
|
self.config.sqlite_path()
|
|
)
|
|
|
|
connection = (
|
|
|
|
self._connect_sqlite(
|
|
|
|
str(
|
|
|
|
database_path
|
|
)
|
|
)
|
|
)
|
|
|
|
try:
|
|
|
|
dataframe = pd.read_sql_query(
|
|
|
|
query,
|
|
|
|
connection
|
|
)
|
|
|
|
finally:
|
|
|
|
connection.close()
|
|
|
|
return dataframe
|
|
|
|
# ==================================================================
|
|
# READ TABLE
|
|
# ==================================================================
|
|
|
|
def read_table(
|
|
self,
|
|
table_name: str,
|
|
database_path: Optional[
|
|
str
|
|
] = None,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Legge una tabella SQLite.
|
|
|
|
Tabelle valide:
|
|
|
|
weather
|
|
panel
|
|
combiner
|
|
inverter
|
|
plant
|
|
faults
|
|
"""
|
|
|
|
allowed_tables = {
|
|
|
|
"weather",
|
|
|
|
"panel",
|
|
|
|
"combiner",
|
|
|
|
"inverter",
|
|
|
|
"plant",
|
|
|
|
"faults",
|
|
}
|
|
|
|
if table_name not in allowed_tables:
|
|
|
|
raise ValueError(
|
|
|
|
"Tabella non valida. "
|
|
f"Valori consentiti: "
|
|
f"{sorted(allowed_tables)}"
|
|
)
|
|
|
|
query = (
|
|
|
|
f"SELECT * "
|
|
f"FROM {table_name}"
|
|
)
|
|
|
|
return self.query_sqlite(
|
|
|
|
query,
|
|
|
|
database_path
|
|
)
|
|
|
|
# ==================================================================
|
|
# READ TIME RANGE
|
|
# ==================================================================
|
|
|
|
def read_time_range(
|
|
self,
|
|
table_name: str,
|
|
start: str,
|
|
end: str,
|
|
database_path: Optional[
|
|
str
|
|
] = None,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Legge i dati di una tabella in un intervallo temporale.
|
|
"""
|
|
|
|
allowed_tables = {
|
|
|
|
"weather",
|
|
|
|
"panel",
|
|
|
|
"combiner",
|
|
|
|
"inverter",
|
|
|
|
"plant",
|
|
|
|
"faults",
|
|
}
|
|
|
|
if table_name not in allowed_tables:
|
|
|
|
raise ValueError(
|
|
|
|
"Tabella non valida."
|
|
)
|
|
|
|
query = f"""
|
|
SELECT *
|
|
FROM {table_name}
|
|
WHERE timestamp >= ?
|
|
AND timestamp < ?
|
|
ORDER BY timestamp
|
|
"""
|
|
|
|
if database_path is None:
|
|
|
|
database_path = (
|
|
|
|
self.config.sqlite_path()
|
|
)
|
|
|
|
connection = (
|
|
|
|
self._connect_sqlite(
|
|
|
|
str(
|
|
|
|
database_path
|
|
)
|
|
)
|
|
)
|
|
|
|
try:
|
|
|
|
dataframe = pd.read_sql_query(
|
|
|
|
query,
|
|
|
|
connection,
|
|
|
|
params=(
|
|
|
|
start,
|
|
|
|
end
|
|
)
|
|
)
|
|
|
|
finally:
|
|
|
|
connection.close()
|
|
|
|
return dataframe
|
|
|
|
# ==================================================================
|
|
# READ COMPONENT
|
|
# ==================================================================
|
|
|
|
def read_component(
|
|
self,
|
|
table_name: str,
|
|
component_id: str,
|
|
database_path: Optional[
|
|
str
|
|
] = None,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Legge la serie temporale di un componente.
|
|
|
|
Esempio:
|
|
|
|
storage.read_component(
|
|
"panel",
|
|
"PANEL_001"
|
|
)
|
|
"""
|
|
|
|
component_columns = {
|
|
|
|
"panel":
|
|
"panel_id",
|
|
|
|
"combiner":
|
|
"combiner_id",
|
|
|
|
"inverter":
|
|
"inverter_id",
|
|
}
|
|
|
|
if table_name not in component_columns:
|
|
|
|
raise ValueError(
|
|
|
|
"La lettura per componente "
|
|
"è supportata per: "
|
|
"panel, combiner, inverter"
|
|
)
|
|
|
|
column = (
|
|
|
|
component_columns[
|
|
table_name
|
|
]
|
|
)
|
|
|
|
query = f"""
|
|
SELECT *
|
|
FROM {table_name}
|
|
WHERE {column} = ?
|
|
ORDER BY timestamp
|
|
"""
|
|
|
|
if database_path is None:
|
|
|
|
database_path = (
|
|
|
|
self.config.sqlite_path()
|
|
)
|
|
|
|
connection = (
|
|
|
|
self._connect_sqlite(
|
|
|
|
str(
|
|
|
|
database_path
|
|
)
|
|
)
|
|
)
|
|
|
|
try:
|
|
|
|
dataframe = pd.read_sql_query(
|
|
|
|
query,
|
|
|
|
connection,
|
|
|
|
params=(
|
|
|
|
component_id,
|
|
)
|
|
)
|
|
|
|
finally:
|
|
|
|
connection.close()
|
|
|
|
return dataframe
|
|
|
|
# ==================================================================
|
|
# PLANT POWER QUERY
|
|
# ==================================================================
|
|
|
|
def read_plant_power(
|
|
self,
|
|
start: Optional[
|
|
str
|
|
] = None,
|
|
end: Optional[
|
|
str
|
|
] = None,
|
|
database_path: Optional[
|
|
str
|
|
] = None,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Restituisce la produzione AC dell'impianto.
|
|
|
|
Se start/end sono specificati,
|
|
filtra l'intervallo temporale.
|
|
"""
|
|
|
|
query = """
|
|
SELECT
|
|
timestamp,
|
|
ac_power_W,
|
|
effective_ac_power_W,
|
|
energy_Wh,
|
|
cumulative_energy_Wh,
|
|
performance_ratio,
|
|
availability,
|
|
fault_active
|
|
FROM plant
|
|
"""
|
|
|
|
params = []
|
|
|
|
conditions = []
|
|
|
|
if start is not None:
|
|
|
|
conditions.append(
|
|
|
|
"timestamp >= ?"
|
|
)
|
|
|
|
params.append(
|
|
|
|
start
|
|
)
|
|
|
|
if end is not None:
|
|
|
|
conditions.append(
|
|
|
|
"timestamp < ?"
|
|
)
|
|
|
|
params.append(
|
|
|
|
end
|
|
)
|
|
|
|
if conditions:
|
|
|
|
query += (
|
|
|
|
" WHERE "
|
|
|
|
+ " AND ".join(
|
|
|
|
conditions
|
|
)
|
|
)
|
|
|
|
query += (
|
|
|
|
" ORDER BY timestamp"
|
|
)
|
|
|
|
if database_path is None:
|
|
|
|
database_path = (
|
|
|
|
self.config.sqlite_path()
|
|
)
|
|
|
|
connection = (
|
|
|
|
self._connect_sqlite(
|
|
|
|
str(
|
|
|
|
database_path
|
|
)
|
|
)
|
|
)
|
|
|
|
try:
|
|
|
|
dataframe = pd.read_sql_query(
|
|
|
|
query,
|
|
|
|
connection,
|
|
|
|
params=params
|
|
)
|
|
|
|
finally:
|
|
|
|
connection.close()
|
|
|
|
return dataframe
|
|
|
|
# ==================================================================
|
|
# FULL SAVE
|
|
# ==================================================================
|
|
|
|
def save_all(
|
|
self,
|
|
result: SimulationResult,
|
|
save_csv: bool = True,
|
|
save_parquet: bool = True,
|
|
save_sqlite: bool = True,
|
|
) -> Dict[
|
|
str,
|
|
object
|
|
]:
|
|
"""
|
|
Salva il risultato in tutti i formati selezionati.
|
|
|
|
Returns
|
|
-------
|
|
Dict
|
|
Dizionario contenente i file generati.
|
|
"""
|
|
|
|
output = {}
|
|
|
|
if save_csv:
|
|
|
|
output[
|
|
"csv"
|
|
] = self.save_csv(
|
|
|
|
result
|
|
)
|
|
|
|
if save_parquet:
|
|
|
|
output[
|
|
"parquet"
|
|
] = self.save_parquet(
|
|
|
|
result
|
|
)
|
|
|
|
if save_sqlite:
|
|
|
|
output[
|
|
"sqlite"
|
|
] = self.save_sqlite(
|
|
|
|
result
|
|
)
|
|
|
|
return output
|
|
|
|
|
|
# ======================================================================
|
|
# UTILITY FUNCTIONS
|
|
# ======================================================================
|
|
|
|
|
|
def save_simulation(
|
|
result: SimulationResult,
|
|
output_dir: str = "output",
|
|
) -> Dict[
|
|
str,
|
|
object
|
|
]:
|
|
"""
|
|
Funzione helper per salvare una simulazione
|
|
in tutti i formati.
|
|
|
|
Esempio:
|
|
|
|
from pvsim.storage import save_simulation
|
|
|
|
files = save_simulation(
|
|
result,
|
|
"output"
|
|
)
|
|
"""
|
|
|
|
storage = SimulationStorage(
|
|
|
|
output_dir=
|
|
|
|
output_dir
|
|
)
|
|
|
|
return storage.save_all(
|
|
|
|
result
|
|
)
|
|
|
|
|
|
# ======================================================================
|
|
# EXPORT
|
|
# ======================================================================
|
|
|
|
|
|
__all__ = [
|
|
|
|
"StorageConfig",
|
|
|
|
"SimulationStorage",
|
|
|
|
"save_simulation",
|
|
]"""
|