added new function
This commit is contained in:
710
cost-calculator.html
Normal file
710
cost-calculator.html
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calcolatore Costi Elettricità Avanzato</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Inter', sans-serif; background-color: #f0f4f8; }
|
||||||
|
.card { background-color: white; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); padding: 1.5rem; margin-bottom: 1.5rem; }
|
||||||
|
.input-label { font-size: 0.875rem; font-weight: 500; color: #4a5568; margin-bottom: 0.5rem; }
|
||||||
|
.input-field, .select-field { width: 100%; padding: 0.75rem 1rem; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 1rem; transition: border-color 0.2s ease-in-out; background-color: white; }
|
||||||
|
.input-field:focus, .select-field:focus { outline: none; border-color: #4299e1; box-shadow: 0 0 0 3px rgba(66,153,225,0.5); }
|
||||||
|
.btn-primary { background-color: #4299e1; color: white; font-weight: 600; padding: 0.75rem 1.5rem; border-radius: 8px; transition: background-color 0.2s ease-in-out; cursor: pointer; border: none; }
|
||||||
|
.btn-primary:hover { background-color: #3182ce; }
|
||||||
|
.output-value { font-size: 1.25rem; font-weight: 700; color: #2c5282; }
|
||||||
|
.output-label { font-size: 0.8rem; color: #718096; }
|
||||||
|
#errorMessage { color: #e53e3e; background-color: #fed7d7; border: 1px solid #f56565; padding: 0.75rem; border-radius: 8px; margin-top: 1rem; text-align: center; }
|
||||||
|
.optional-section { border-top: 1px dashed #cbd5e0; margin-top: 1rem; padding-top: 1rem; }
|
||||||
|
.checkbox-label { font-size: 0.9rem; font-weight: 500; color: #4a5568; margin-left: 0.5rem; }
|
||||||
|
.lang-selector { padding: 0.3rem 0.6rem; font-size: 0.8rem; margin-left: 0.5rem; border-radius: 6px; }
|
||||||
|
.device-schedule-section { border-top: 1px solid #e2e8f0; margin-top: 1rem; padding-top: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex flex-col items-center justify-center p-4 sm:p-6">
|
||||||
|
|
||||||
|
<div class="w-full max-w-4xl">
|
||||||
|
<header class="text-center mb-6 relative">
|
||||||
|
<div class="absolute top-0 right-0 mt-2 mr-2">
|
||||||
|
<select id="languageSelector" class="select-field lang-selector">
|
||||||
|
<option value="it">Italiano</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<h1 id="mainTitle" class="text-3xl sm:text-4xl font-bold text-gray-800 pt-8">Calcolo Avanzato Consumi e Costi Elettricità</h1>
|
||||||
|
<p id="subTitle" class="text-gray-600 mt-2">Includi fotovoltaico e batteria per una stima più completa.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Colonna Input -->
|
||||||
|
<div class="lg:col-span-1 card">
|
||||||
|
<h2 id="dataInputTitle" class="text-xl font-semibold text-gray-700 mb-6">Inserisci i Dati</h2>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label for="power" id="labelDevicePower" class="input-label block">Consumo Medio Dispositivo (Watt)</label>
|
||||||
|
<input type="number" id="power" class="input-field" placeholder="es. 100" value="100">
|
||||||
|
</div>
|
||||||
|
<div class="device-schedule-section">
|
||||||
|
<label for="deviceStartHour" id="labelDeviceStartHour" class="input-label block">Ora Inizio Funzionamento Dispositivo</label>
|
||||||
|
<input type="number" id="deviceStartHour" class="input-field" placeholder="0-23 (es. 8)" value="8" min="0" max="23">
|
||||||
|
|
||||||
|
<label for="deviceOperatingHours" id="labelDeviceOpHours" class="input-label block mt-2">Durata Funzionamento Dispositivo (Ore)</label>
|
||||||
|
<input type="number" id="deviceOperatingHours" class="input-field" placeholder="1-24 (es. 10)" value="10" min="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="cost" id="labelGridCost" class="input-label block mt-3">Costo Energia da Rete (€/kWh)</label>
|
||||||
|
<input type="number" id="cost" class="input-field" placeholder="es. 0,25" value="0.25" step="0.01">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="time" id="labelSimDuration" class="input-label block">Durata Simulazione (Ore)</label>
|
||||||
|
<input type="number" id="time" class="input-field" placeholder="es. 24" value="24" min="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sezione Fotovoltaico -->
|
||||||
|
<div class="optional-section">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<input type="checkbox" id="includePv" class="rounded">
|
||||||
|
<label for="includePv" id="labelIncludePv" class="checkbox-label">Includi Impianto Fotovoltaico</label>
|
||||||
|
</div>
|
||||||
|
<div id="pvInputs" class="hidden space-y-3 pl-2">
|
||||||
|
<div>
|
||||||
|
<label for="pvPeakPower" id="labelPvPeakPower" class="input-label block">Potenza Picco Impianto FV (kWp)</label>
|
||||||
|
<input type="number" id="pvPeakPower" class="input-field" placeholder="es. 3" value="3" step="0.1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="geographicZone" id="labelGeoZone" class="input-label block">Paese / Regione</label>
|
||||||
|
<select id="geographicZone" class="select-field">
|
||||||
|
<!-- Options will be populated by JS -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="season" id="labelSeason" class="input-label block">Stagione</label>
|
||||||
|
<select id="season" class="select-field">
|
||||||
|
<!-- Options will be populated by JS -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="dayType" id="labelDayType" class="input-label block">Tipo di Giornata</label>
|
||||||
|
<select id="dayType" class="select-field">
|
||||||
|
<!-- Options will be populated by JS -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mt-1">
|
||||||
|
<input type="checkbox" id="useSimulatedWeather" class="rounded">
|
||||||
|
<label for="useSimulatedWeather" id="labelUseSimulatedWeather" class="checkbox-label text-xs">Usa Meteo Attuale (Simulato)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sezione Batteria -->
|
||||||
|
<div class="optional-section">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<input type="checkbox" id="includeBattery" class="rounded">
|
||||||
|
<label for="includeBattery" id="labelIncludeBattery" class="checkbox-label">Includi Batteria di Accumulo</label>
|
||||||
|
</div>
|
||||||
|
<div id="batteryInputs" class="hidden space-y-3 pl-2">
|
||||||
|
<div>
|
||||||
|
<label for="batteryCapacity" id="labelBatteryCapacity" class="input-label block">Capacità Batteria (kWh)</label>
|
||||||
|
<input type="number" id="batteryCapacity" class="input-field" placeholder="es. 5" value="5" step="0.1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="initialBatteryCharge" id="labelInitialBattery" class="input-label block">Carica Iniziale Batteria (%)</label>
|
||||||
|
<input type="number" id="initialBatteryCharge" class="input-field" placeholder="es. 50" value="50" min="0" max="100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="calculateBtn" class="btn-primary w-full mt-6">Calcola</button>
|
||||||
|
<div id="errorMessage" class="hidden mt-4"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna Output e Grafico -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div class="card">
|
||||||
|
<h2 id="resultsTitle" class="text-xl font-semibold text-gray-700 mb-4">Risultati Stimati</h2>
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-4">
|
||||||
|
<div class="text-center p-2 bg-gray-50 rounded-lg">
|
||||||
|
<p id="energyConsumedDevice" class="output-value">--</p>
|
||||||
|
<p id="labelDeviceConsumption" class="output-label">Consumo Dispositivo</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-2 bg-green-50 rounded-lg">
|
||||||
|
<p id="energyFromPv" class="output-value">--</p>
|
||||||
|
<p id="labelEnergyFromPv" class="output-label">Energia da FV</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-2 bg-blue-50 rounded-lg">
|
||||||
|
<p id="energyFromBattery" class="output-value">--</p>
|
||||||
|
<p id="labelEnergyFromBattery" class="output-label">Energia da Batteria</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-2 bg-orange-50 rounded-lg">
|
||||||
|
<p id="energyToBattery" class="output-value">--</p>
|
||||||
|
<p id="labelEnergyToBattery" class="output-label">Energia a Batteria</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-2 bg-red-50 rounded-lg">
|
||||||
|
<p id="energyFromGrid" class="output-value">--</p>
|
||||||
|
<p id="labelEnergyFromGrid" class="output-label">Prelievo da Rete</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-2 bg-purple-50 rounded-lg">
|
||||||
|
<p id="totalCost" class="output-value">--</p>
|
||||||
|
<p id="labelTotalGridCost" class="output-label">Costo Totale Rete</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="simulationNote" class="text-xs text-gray-500 mt-3 text-center">
|
||||||
|
Nota: il consumo del dispositivo avviene solo nelle ore specificate e presenta fluttuazioni (5%). La produzione FV è stimata (con fluttuazioni dinamiche basate su zona/stagione/tipo giornata) considerando un ciclo giorno/notte.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-6 w-full">
|
||||||
|
<h2 id="chartTitle" class="text-xl font-semibold text-gray-700 mb-4 text-center">Andamento Energetico Simulato</h2>
|
||||||
|
<div class="relative h-72 sm:h-96">
|
||||||
|
<canvas id="consumptionChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<p id="chartMessage" class="text-sm text-gray-500 mt-4 text-center">Premi "Calcola" per generare il grafico.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="text-center mt-8 text-sm text-gray-500">
|
||||||
|
<p id="footerText">© 2024 Calcolatore Energetico Avanzato. Solo a scopo illustrativo.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// --- DOM Elements ---
|
||||||
|
const languageSelector = document.getElementById('languageSelector');
|
||||||
|
const powerInput = document.getElementById('power');
|
||||||
|
const deviceStartHourInput = document.getElementById('deviceStartHour');
|
||||||
|
const deviceOperatingHoursInput = document.getElementById('deviceOperatingHours');
|
||||||
|
const costInput = document.getElementById('cost');
|
||||||
|
const timeInput = document.getElementById('time');
|
||||||
|
const includePvCheckbox = document.getElementById('includePv');
|
||||||
|
const pvInputsDiv = document.getElementById('pvInputs');
|
||||||
|
const pvPeakPowerInput = document.getElementById('pvPeakPower');
|
||||||
|
const seasonSelect = document.getElementById('season');
|
||||||
|
const geographicZoneSelect = document.getElementById('geographicZone');
|
||||||
|
const dayTypeSelect = document.getElementById('dayType');
|
||||||
|
const useSimulatedWeatherCheckbox = document.getElementById('useSimulatedWeather');
|
||||||
|
const includeBatteryCheckbox = document.getElementById('includeBattery');
|
||||||
|
const batteryInputsDiv = document.getElementById('batteryInputs');
|
||||||
|
const batteryCapacityInput = document.getElementById('batteryCapacity');
|
||||||
|
const initialBatteryChargeInput = document.getElementById('initialBatteryCharge');
|
||||||
|
const calculateBtn = document.getElementById('calculateBtn');
|
||||||
|
const energyConsumedDeviceOutput = document.getElementById('energyConsumedDevice');
|
||||||
|
const energyFromPvOutput = document.getElementById('energyFromPv');
|
||||||
|
const energyFromBatteryOutput = document.getElementById('energyFromBattery');
|
||||||
|
const energyToBatteryOutput = document.getElementById('energyToBattery');
|
||||||
|
const energyFromGridOutput = document.getElementById('energyFromGrid');
|
||||||
|
const totalCostOutput = document.getElementById('totalCost');
|
||||||
|
const consumptionChartCanvas = document.getElementById('consumptionChart');
|
||||||
|
const errorMessageDiv = document.getElementById('errorMessage');
|
||||||
|
const chartMessageP = document.getElementById('chartMessage');
|
||||||
|
|
||||||
|
let consumptionChart = null;
|
||||||
|
let currentLang = 'it';
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
it: {
|
||||||
|
docTitle: "Calcolatore Costi Elettricità Avanzato",
|
||||||
|
mainTitle: "Calcolo Avanzato Consumi e Costi Elettricità",
|
||||||
|
subTitle: "Includi fotovoltaico e batteria per una stima più completa.",
|
||||||
|
dataInputTitle: "Inserisci i Dati",
|
||||||
|
labelDevicePower: "Consumo Medio Dispositivo (Watt, durante funzionamento)",
|
||||||
|
labelDeviceStartHour: "Ora Inizio Funzionamento Dispositivo (0-23)",
|
||||||
|
labelDeviceOpHours: "Durata Funzionamento Dispositivo (Ore)",
|
||||||
|
labelGridCost: "Costo Energia da Rete (€/kWh)",
|
||||||
|
labelSimDuration: "Durata Intera Simulazione (Ore)",
|
||||||
|
labelIncludePv: "Includi Impianto Fotovoltaico",
|
||||||
|
labelPvPeakPower: "Potenza Picco Impianto FV (kWp)",
|
||||||
|
labelSeason: "Stagione",
|
||||||
|
labelGeoZone: "Paese / Regione",
|
||||||
|
labelDayType: "Tipo di Giornata",
|
||||||
|
labelUseSimulatedWeather: "Usa Meteo Attuale (Simulato)",
|
||||||
|
labelIncludeBattery: "Includi Batteria di Accumulo",
|
||||||
|
labelBatteryCapacity: "Capacità Batteria (kWh)",
|
||||||
|
labelInitialBattery: "Carica Iniziale Batteria (%)",
|
||||||
|
calculateBtn: "Calcola",
|
||||||
|
resultsTitle: "Risultati Stimati",
|
||||||
|
labelDeviceConsumption: "Consumo Dispositivo",
|
||||||
|
labelEnergyFromPv: "Energia da FV",
|
||||||
|
labelEnergyFromBattery: "Energia da Batteria",
|
||||||
|
labelEnergyToBattery: "Energia a Batteria",
|
||||||
|
labelEnergyFromGrid: "Prelievo da Rete",
|
||||||
|
labelTotalGridCost: "Costo Totale Rete",
|
||||||
|
simulationNote: "Nota: il consumo del dispositivo avviene solo nelle ore specificate e presenta fluttuazioni (5%). La produzione FV è stimata (con fluttuazioni dinamiche basate su zona/stagione/tipo giornata) considerando un ciclo giorno/notte.",
|
||||||
|
chartTitle: "Andamento Energetico Simulato",
|
||||||
|
chartMessage: 'Premi "Calcola" per generare il grafico.',
|
||||||
|
footerText: "© 2024 Calcolatore Energetico Avanzato. Solo a scopo illustrativo.",
|
||||||
|
errorDevicePower: "Inserisci una potenza valida per il dispositivo (>0 Watt).",
|
||||||
|
errorDeviceStartHour: "Ora inizio funzionamento dispositivo non valida (0-23).",
|
||||||
|
errorDeviceOpHours: "Durata funzionamento dispositivo non valida (>0 ore).",
|
||||||
|
errorGridCost: "Inserisci un costo per kWh valido (>=0).",
|
||||||
|
errorSimDuration: "Inserisci una durata simulazione valida (>0 ore).",
|
||||||
|
errorPvPeakPower: "Inserisci una potenza di picco FV valida (>0 kWp).",
|
||||||
|
errorPvCombination: "Combinazione zona/stagione/tipo giornata non valida per FV.",
|
||||||
|
errorBatteryCapacity: "Inserisci una capacità batteria valida (>0 kWh).",
|
||||||
|
errorInitialBattery: "Inserisci una carica iniziale batteria valida (0-100%).",
|
||||||
|
chartLabelDeviceConsumption: "Consumo Dispositivo (W)",
|
||||||
|
chartLabelAvgDevicePower: "Potenza Media Dispositivo (W, se attivo)",
|
||||||
|
chartLabelGridDraw: "Prelievo da Rete (W)",
|
||||||
|
chartLabelPvProduction: "Produzione FV (W)",
|
||||||
|
chartLabelAvgPvProduction: "Produzione Media FV Attesa (W, su 24h)",
|
||||||
|
chartLabelBatteryCharge: "Carica Batteria (%)",
|
||||||
|
chartAxisPower: "Potenza (Watt)",
|
||||||
|
chartAxisBattery: "Carica Batteria (%)",
|
||||||
|
chartAxisDuration: "Durata (Ore)",
|
||||||
|
seasons: { inverno: "Inverno", mezzaStagione: "Primavera/Autunno", estate: "Estate" },
|
||||||
|
dayTypes: { sunny: "Soleggiata", partlyCloudy: "Parz. Nuvolosa", cloudy: "Nuvolosa/Pioggia" },
|
||||||
|
geoZones: {
|
||||||
|
it_north: "Italia - Nord", it_center: "Italia - Centro", it_south: "Italia - Sud/Isole",
|
||||||
|
de_north: "Germania - Nord", de_south: "Germania - Sud",
|
||||||
|
es_south: "Spagna - Sud", fr_north: "Francia - Nord",
|
||||||
|
uk_south: "Regno Unito - Sud",
|
||||||
|
usa_ca: "USA - California", usa_ny: "USA - New York",
|
||||||
|
eq_generic: "Zona Equatoriale Generica"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
docTitle: "Advanced Electricity Cost Calculator",
|
||||||
|
mainTitle: "Advanced Electricity Consumption and Cost Calculator",
|
||||||
|
subTitle: "Include photovoltaics and battery for a more complete estimate.",
|
||||||
|
dataInputTitle: "Enter Your Data",
|
||||||
|
labelDevicePower: "Average Device Consumption (Watts, when ON)",
|
||||||
|
labelDeviceStartHour: "Device Operating Start Hour (0-23)",
|
||||||
|
labelDeviceOpHours: "Device Operating Duration (Hours)",
|
||||||
|
labelGridCost: "Grid Energy Cost (€/kWh)",
|
||||||
|
labelSimDuration: "Total Simulation Duration (Hours)",
|
||||||
|
labelIncludePv: "Include Photovoltaic System",
|
||||||
|
labelPvPeakPower: "PV System Peak Power (kWp)",
|
||||||
|
labelSeason: "Season",
|
||||||
|
labelGeoZone: "Country / Region",
|
||||||
|
labelDayType: "Day Type",
|
||||||
|
labelUseSimulatedWeather: "Use Current Weather (Simulated)",
|
||||||
|
labelIncludeBattery: "Include Storage Battery",
|
||||||
|
labelBatteryCapacity: "Battery Capacity (kWh)",
|
||||||
|
labelInitialBattery: "Initial Battery Charge (%)",
|
||||||
|
calculateBtn: "Calculate",
|
||||||
|
resultsTitle: "Estimated Results",
|
||||||
|
labelDeviceConsumption: "Device Consumption",
|
||||||
|
labelEnergyFromPv: "Energy from PV",
|
||||||
|
labelEnergyFromBattery: "Energy from Battery",
|
||||||
|
labelEnergyToBattery: "Energy to Battery",
|
||||||
|
labelEnergyFromGrid: "Grid Draw",
|
||||||
|
labelTotalGridCost: "Total Grid Cost",
|
||||||
|
simulationNote: "Note: Device consumption occurs only during specified hours and includes fluctuations (5%). PV production is estimated (with dynamic fluctuations based on zone/season/day type) considering a day/night cycle.",
|
||||||
|
chartTitle: "Simulated Energy Flow",
|
||||||
|
chartMessage: 'Press "Calculate" to generate the chart.',
|
||||||
|
footerText: "© 2024 Advanced Energy Calculator. For illustrative purposes only.",
|
||||||
|
errorDevicePower: "Enter a valid device power (>0 Watts).",
|
||||||
|
errorDeviceStartHour: "Invalid device start hour (0-23).",
|
||||||
|
errorDeviceOpHours: "Invalid device operating duration (>0 hours).",
|
||||||
|
errorGridCost: "Enter a valid cost per kWh (>=0).",
|
||||||
|
errorSimDuration: "Enter a valid simulation duration (>0 hours).",
|
||||||
|
errorPvPeakPower: "Enter a valid PV peak power (>0 kWp).",
|
||||||
|
errorPvCombination: "Invalid zone/season/day type combination for PV.",
|
||||||
|
errorBatteryCapacity: "Enter a valid battery capacity (>0 kWh).",
|
||||||
|
errorInitialBattery: "Enter a valid initial battery charge (0-100%).",
|
||||||
|
chartLabelDeviceConsumption: "Device Consumption (W)",
|
||||||
|
chartLabelAvgDevicePower: "Avg Device Power (W, if ON)",
|
||||||
|
chartLabelGridDraw: "Grid Draw (W)",
|
||||||
|
chartLabelPvProduction: "PV Production (W)",
|
||||||
|
chartLabelAvgPvProduction: "Avg Expected PV Production (W, 24h avg)",
|
||||||
|
chartLabelBatteryCharge: "Battery Charge (%)",
|
||||||
|
chartAxisPower: "Power (Watts)",
|
||||||
|
chartAxisBattery: "Battery Charge (%)",
|
||||||
|
chartAxisDuration: "Duration (Hours)",
|
||||||
|
seasons: { inverno: "Winter", mezzaStagione: "Spring/Autumn", estate: "Summer" },
|
||||||
|
dayTypes: { sunny: "Sunny", partlyCloudy: "Partly Cloudy", cloudy: "Cloudy/Rainy" },
|
||||||
|
geoZones: {
|
||||||
|
it_north: "Italy - North", it_center: "Italy - Center", it_south: "Italy - South/Islands",
|
||||||
|
de_north: "Germany - North", de_south: "Germany - South",
|
||||||
|
es_south: "Spain - South", fr_north: "France - North",
|
||||||
|
uk_south: "United Kingdom - South",
|
||||||
|
usa_ca: "USA - California", usa_ny: "USA - New York",
|
||||||
|
eq_generic: "Generic Equatorial Zone"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// kWh/kWp/day for a TYPICALLY SUNNY DAY
|
||||||
|
const yieldFactors = {
|
||||||
|
it_north: { inverno: 1.2, mezzaStagione: 3.0, estate: 4.8 },
|
||||||
|
it_center: { inverno: 1.8, mezzaStagione: 4.0, estate: 6.0 }, // User's reference
|
||||||
|
it_south: { inverno: 2.5, mezzaStagione: 4.8, estate: 7.0 },
|
||||||
|
de_north: { inverno: 0.8, mezzaStagione: 2.5, estate: 4.2 },
|
||||||
|
de_south: { inverno: 1.0, mezzaStagione: 3.0, estate: 4.8 },
|
||||||
|
es_south: { inverno: 2.8, mezzaStagione: 5.0, estate: 7.5 },
|
||||||
|
fr_north: { inverno: 1.0, mezzaStagione: 2.9, estate: 4.6 },
|
||||||
|
uk_south: { inverno: 0.9, mezzaStagione: 2.7, estate: 4.3 },
|
||||||
|
usa_ca: { inverno: 3.0, mezzaStagione: 5.5, estate: 7.8 },
|
||||||
|
usa_ny: { inverno: 1.5, mezzaStagione: 3.8, estate: 5.2 },
|
||||||
|
eq_generic:{ inverno: 4.2, mezzaStagione: 4.5, estate: 4.8 } // Less seasonal variation, more consistent
|
||||||
|
};
|
||||||
|
|
||||||
|
// Defines daylight window and reference hours for calculating a higher power base for fluctuations
|
||||||
|
const seasonalDaylightAndPowerRefHours = {
|
||||||
|
inverno: { start: 8, end: 17, refHoursForPowerCalc: 4 }, // Adjusted refHours
|
||||||
|
mezzaStagione: { start: 7, end: 19, refHoursForPowerCalc: 6 },
|
||||||
|
estate: { start: 6, end: 20, refHoursForPowerCalc: 8 } // Longer ref for summer
|
||||||
|
};
|
||||||
|
|
||||||
|
// +/- percentage range, relative to the curve generated for the day type
|
||||||
|
const pvBaseFluctuation = 0.10; // Base random noise on top of the shaped curve
|
||||||
|
const dayTypeModifiers = {
|
||||||
|
// Factor to multiply daily energy yield by, and base variability factor
|
||||||
|
sunny: { yieldFactor: 1.0, variabilityFactor: 0.15, shapeIntensity: 1.0 }, // Shape intensity for how "peaky"
|
||||||
|
partlyCloudy: { yieldFactor: 0.6, variabilityFactor: 0.40, shapeIntensity: 0.7 },
|
||||||
|
cloudy: { yieldFactor: 0.25, variabilityFactor: 0.65, shapeIntensity: 0.4 }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function updateUI(lang) {
|
||||||
|
currentLang = lang;
|
||||||
|
document.documentElement.lang = lang;
|
||||||
|
const t = translations[lang];
|
||||||
|
|
||||||
|
document.title = t.docTitle;
|
||||||
|
document.getElementById('mainTitle').textContent = t.mainTitle;
|
||||||
|
document.getElementById('subTitle').textContent = t.subTitle;
|
||||||
|
document.getElementById('dataInputTitle').textContent = t.dataInputTitle;
|
||||||
|
document.getElementById('labelDevicePower').textContent = t.labelDevicePower;
|
||||||
|
document.getElementById('labelDeviceStartHour').textContent = t.labelDeviceStartHour;
|
||||||
|
document.getElementById('labelDeviceOpHours').textContent = t.labelDeviceOpHours;
|
||||||
|
document.getElementById('labelGridCost').textContent = t.labelGridCost;
|
||||||
|
document.getElementById('labelSimDuration').textContent = t.labelSimDuration;
|
||||||
|
document.getElementById('labelIncludePv').textContent = t.labelIncludePv;
|
||||||
|
document.getElementById('labelPvPeakPower').textContent = t.labelPvPeakPower;
|
||||||
|
document.getElementById('labelSeason').textContent = t.labelSeason;
|
||||||
|
document.getElementById('labelGeoZone').textContent = t.labelGeoZone;
|
||||||
|
document.getElementById('labelDayType').textContent = t.labelDayType;
|
||||||
|
document.getElementById('labelUseSimulatedWeather').textContent = t.labelUseSimulatedWeather;
|
||||||
|
document.getElementById('labelIncludeBattery').textContent = t.labelIncludeBattery;
|
||||||
|
document.getElementById('labelBatteryCapacity').textContent = t.labelBatteryCapacity;
|
||||||
|
document.getElementById('labelInitialBattery').textContent = t.labelInitialBattery;
|
||||||
|
calculateBtn.textContent = t.calculateBtn;
|
||||||
|
document.getElementById('resultsTitle').textContent = t.resultsTitle;
|
||||||
|
document.getElementById('labelDeviceConsumption').textContent = t.labelDeviceConsumption;
|
||||||
|
document.getElementById('labelEnergyFromPv').textContent = t.labelEnergyFromPv;
|
||||||
|
document.getElementById('labelEnergyFromBattery').textContent = t.labelEnergyFromBattery;
|
||||||
|
document.getElementById('labelEnergyToBattery').textContent = t.labelEnergyToBattery;
|
||||||
|
document.getElementById('labelEnergyFromGrid').textContent = t.labelEnergyFromGrid;
|
||||||
|
document.getElementById('labelTotalGridCost').textContent = t.labelTotalGridCost;
|
||||||
|
document.getElementById('simulationNote').textContent = t.simulationNote;
|
||||||
|
document.getElementById('chartTitle').textContent = t.chartTitle;
|
||||||
|
chartMessageP.textContent = t.chartMessage;
|
||||||
|
document.getElementById('footerText').textContent = t.footerText;
|
||||||
|
|
||||||
|
seasonSelect.innerHTML = '';
|
||||||
|
for (const key in t.seasons) {
|
||||||
|
const option = document.createElement('option'); option.value = key; option.textContent = t.seasons[key];
|
||||||
|
if (key === 'mezzaStagione') option.selected = true; seasonSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
geographicZoneSelect.innerHTML = '';
|
||||||
|
for (const key in t.geoZones) {
|
||||||
|
const option = document.createElement('option'); option.value = key; option.textContent = t.geoZones[key];
|
||||||
|
// Default to Italy Center if available, else first
|
||||||
|
if (key === 'it_center' || (geographicZoneSelect.options.length === 0 && key === Object.keys(t.geoZones)[0])) option.selected = true;
|
||||||
|
geographicZoneSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
dayTypeSelect.innerHTML = '';
|
||||||
|
for (const key in t.dayTypes) {
|
||||||
|
const option = document.createElement('option'); option.value = key; option.textContent = t.dayTypes[key];
|
||||||
|
if (key === 'sunny') option.selected = true; dayTypeSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
if (consumptionChart) { calculateBtn.click(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
languageSelector.addEventListener('change', (event) => updateUI(event.target.value));
|
||||||
|
includePvCheckbox.addEventListener('change', () => {
|
||||||
|
pvInputsDiv.classList.toggle('hidden', !includePvCheckbox.checked);
|
||||||
|
if (!includePvCheckbox.checked) {
|
||||||
|
includeBatteryCheckbox.checked = false;
|
||||||
|
batteryInputsDiv.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
includeBatteryCheckbox.addEventListener('change', () => batteryInputsDiv.classList.toggle('hidden', !includeBatteryCheckbox.checked));
|
||||||
|
|
||||||
|
function displayError(messageKey, lang = currentLang) {
|
||||||
|
errorMessageDiv.textContent = translations[lang][messageKey] || messageKey;
|
||||||
|
errorMessageDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
function clearError() { errorMessageDiv.classList.add('hidden'); errorMessageDiv.textContent = ''; }
|
||||||
|
function formatEnergy(wh) {
|
||||||
|
if (wh === null || isNaN(wh)) return '--';
|
||||||
|
if (Math.abs(wh) < 1000) return wh.toFixed(1) + " Wh";
|
||||||
|
if (Math.abs(wh) < 1000000) return (wh / 1000).toFixed(2) + " kWh";
|
||||||
|
return (wh / 1000000).toFixed(3) + " MWh";
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBtn.addEventListener('click', () => {
|
||||||
|
clearError();
|
||||||
|
chartMessageP.classList.add('hidden');
|
||||||
|
const t = translations[currentLang];
|
||||||
|
|
||||||
|
const powerDeviceAvgW = parseFloat(powerInput.value);
|
||||||
|
const deviceStartHour = parseInt(deviceStartHourInput.value, 10);
|
||||||
|
const deviceOpHours = parseFloat(deviceOperatingHoursInput.value);
|
||||||
|
const costPerKwh = parseFloat(costInput.value.replace(',', '.'));
|
||||||
|
const totalHours = parseFloat(timeInput.value);
|
||||||
|
|
||||||
|
let errors = [];
|
||||||
|
if (isNaN(powerDeviceAvgW) || powerDeviceAvgW <= 0) errors.push("errorDevicePower");
|
||||||
|
if (isNaN(deviceStartHour) || deviceStartHour < 0 || deviceStartHour > 23) errors.push("errorDeviceStartHour");
|
||||||
|
if (isNaN(deviceOpHours) || deviceOpHours <= 0 ) errors.push("errorDeviceOpHours");
|
||||||
|
if (isNaN(costPerKwh) || costPerKwh < 0) errors.push("errorGridCost");
|
||||||
|
if (isNaN(totalHours) || totalHours <= 0) errors.push("errorSimDuration");
|
||||||
|
|
||||||
|
const usePv = includePvCheckbox.checked;
|
||||||
|
let pvPeakPowerkWp = 0, season = seasonSelect.value, zone = geographicZoneSelect.value, dayType = dayTypeSelect.value;
|
||||||
|
let useSimWeather = useSimulatedWeatherCheckbox.checked;
|
||||||
|
|
||||||
|
let avgDailyPvEnergyWh_target_base = 0;
|
||||||
|
let pvDaylightConfig = seasonalDaylightAndPowerRefHours[season] || seasonalDaylightAndPowerRefHours.mezzaStagione;
|
||||||
|
let currentDayTypeModifier = dayTypeModifiers[dayType] || dayTypeModifiers.sunny;
|
||||||
|
|
||||||
|
if (usePv) {
|
||||||
|
pvPeakPowerkWp = parseFloat(pvPeakPowerInput.value);
|
||||||
|
if (isNaN(pvPeakPowerkWp) || pvPeakPowerkWp <= 0) errors.push("errorPvPeakPower");
|
||||||
|
|
||||||
|
const currentYieldFactors = yieldFactors[zone];
|
||||||
|
if (currentYieldFactors && currentYieldFactors[season] !== undefined) {
|
||||||
|
const selectedYieldFactor = currentYieldFactors[season];
|
||||||
|
avgDailyPvEnergyWh_target_base = pvPeakPowerkWp * 1000 * selectedYieldFactor; // Energy for a sunny day
|
||||||
|
} else {
|
||||||
|
errors.push("errorPvCombination");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBattery = includeBatteryCheckbox.checked;
|
||||||
|
let batteryCapacityKWh = 0, currentBatteryChargePercentage = 0;
|
||||||
|
let batteryMaxEnergyWh = 0, currentBatteryEnergyWh = 0;
|
||||||
|
if (useBattery) {
|
||||||
|
batteryCapacityKWh = parseFloat(batteryCapacityInput.value);
|
||||||
|
currentBatteryChargePercentage = parseFloat(initialBatteryChargeInput.value);
|
||||||
|
if (isNaN(batteryCapacityKWh) || batteryCapacityKWh <= 0) errors.push("errorBatteryCapacity");
|
||||||
|
if (isNaN(currentBatteryChargePercentage) || currentBatteryChargePercentage < 0 || currentBatteryChargePercentage > 100) errors.push("errorInitialBattery");
|
||||||
|
batteryMaxEnergyWh = batteryCapacityKWh * 1000;
|
||||||
|
currentBatteryEnergyWh = batteryMaxEnergyWh * (currentBatteryChargePercentage / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) { displayError(errors[0]); return; }
|
||||||
|
|
||||||
|
const numberOfIntervals = Math.max(Math.ceil(totalHours * 4), 96); // Higher resolution for PV curve
|
||||||
|
const timePerIntervalHours = totalHours / numberOfIntervals;
|
||||||
|
const deviceFluctuationPercentage = 0.05;
|
||||||
|
|
||||||
|
// Apply day type modifier to the target daily energy
|
||||||
|
let avgDailyPvEnergyWh_target_adjusted = avgDailyPvEnergyWh_target_base * currentDayTypeModifier.yieldFactor;
|
||||||
|
|
||||||
|
// Apply simulated weather modifier
|
||||||
|
if (usePv && useSimWeather) {
|
||||||
|
const weatherModifier = 0.8 + Math.random() * 0.4; // Random factor between 0.8 (-20%) and 1.2 (+20%)
|
||||||
|
avgDailyPvEnergyWh_target_adjusted *= weatherModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pvPowerBaseForCurveShape = 0;
|
||||||
|
if (usePv && pvDaylightConfig.refHoursForPowerCalc > 0) {
|
||||||
|
pvPowerBaseForCurveShape = avgDailyPvEnergyWh_target_adjusted / pvDaylightConfig.refHoursForPowerCalc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let totalDeviceEnergyWh = 0, simulatedTotalPvEnergyWh_beforeScaling = 0, totalEnergyDrawnFromBatteryWh = 0;
|
||||||
|
let totalEnergyChargedToBatteryWh = 0, totalGridEnergyWh = 0;
|
||||||
|
|
||||||
|
const timeLabels = [], deviceConsumptionData = [], pvProductionData_raw = [], batteryChargeData = [], gridConsumptionData = [];
|
||||||
|
|
||||||
|
// --- Simulation Loop (Phase 1: Calculate Raw PV Production based on curve shape) ---
|
||||||
|
for (let i = 0; i < numberOfIntervals; i++) {
|
||||||
|
const currentIntervalStartSimTime = i * timePerIntervalHours;
|
||||||
|
const currentHourOfDay = currentIntervalStartSimTime % 24;
|
||||||
|
|
||||||
|
let intervalPvPowerW_shaped = 0;
|
||||||
|
if (usePv) {
|
||||||
|
if (currentHourOfDay >= pvDaylightConfig.start && currentHourOfDay < pvDaylightConfig.end) {
|
||||||
|
// Bell-curve shaping (simplified sinusoidal)
|
||||||
|
const hoursIntoDaylight = currentHourOfDay - pvDaylightConfig.start;
|
||||||
|
const daylightDuration = pvDaylightConfig.end - pvDaylightConfig.start;
|
||||||
|
const peakHour = pvDaylightConfig.start + daylightDuration / 2;
|
||||||
|
|
||||||
|
// More pronounced peak for sunny days, flatter for cloudy
|
||||||
|
let shapeFactor = Math.sin((Math.PI * hoursIntoDaylight) / daylightDuration);
|
||||||
|
shapeFactor = Math.pow(shapeFactor, currentDayTypeModifier.shapeIntensity); // Sharpen/flatten peak
|
||||||
|
|
||||||
|
intervalPvPowerW_shaped = pvPowerBaseForCurveShape * shapeFactor;
|
||||||
|
|
||||||
|
// Add random fluctuation based on day type variability
|
||||||
|
const randomFluctuation = (Math.random() * 2 - 1) * currentDayTypeModifier.variabilityFactor;
|
||||||
|
intervalPvPowerW_shaped *= (1 + randomFluctuation);
|
||||||
|
|
||||||
|
intervalPvPowerW_shaped = Math.max(0, Math.min(intervalPvPowerW_shaped, pvPeakPowerkWp * 1000));
|
||||||
|
} else {
|
||||||
|
intervalPvPowerW_shaped = 0; // Night time
|
||||||
|
}
|
||||||
|
const intervalPvEnergyWh = intervalPvPowerW_shaped * timePerIntervalHours;
|
||||||
|
simulatedTotalPvEnergyWh_beforeScaling += intervalPvEnergyWh;
|
||||||
|
pvProductionData_raw.push(intervalPvPowerW_shaped);
|
||||||
|
} else {
|
||||||
|
pvProductionData_raw.push(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PV Energy Reconciliation (Phase 2) ---
|
||||||
|
let pvScalingFactor = 1;
|
||||||
|
if (usePv && simulatedTotalPvEnergyWh_beforeScaling > 0.001) { // Avoid division by zero or tiny numbers
|
||||||
|
const numberOfSimulatedDays = totalHours / 24;
|
||||||
|
const targetTotalPvEnergyWh_forSimDuration = avgDailyPvEnergyWh_target_adjusted * numberOfSimulatedDays;
|
||||||
|
pvScalingFactor = targetTotalPvEnergyWh_forSimDuration / simulatedTotalPvEnergyWh_beforeScaling;
|
||||||
|
} else if (usePv && avgDailyPvEnergyWh_target_adjusted > 0) { // Target was >0 but simulated was 0
|
||||||
|
pvScalingFactor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const pvProductionData_scaled = pvProductionData_raw.map(p => Math.max(0, p * pvScalingFactor)); // Ensure no negative after scaling
|
||||||
|
let finalTotalPvEnergyWh = 0;
|
||||||
|
if(usePv) {
|
||||||
|
pvProductionData_scaled.forEach(power => {
|
||||||
|
finalTotalPvEnergyWh += power * timePerIntervalHours;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Simulation Loop (Phase 3: Device, Battery, Grid with Scaled PV) ---
|
||||||
|
// Reset totals for the main simulation pass
|
||||||
|
totalDeviceEnergyWh = 0;
|
||||||
|
totalGridEnergyWh = 0;
|
||||||
|
// totalEnergyDrawnFromBatteryWh and totalEnergyChargedToBatteryWh are reset implicitly by not accumulating from prev runs
|
||||||
|
// currentBatteryEnergyWh is reset to initial for each calculation run
|
||||||
|
|
||||||
|
if (useBattery) { // Reset battery for this simulation run
|
||||||
|
currentBatteryEnergyWh = batteryMaxEnergyWh * (currentBatteryChargePercentage / 100);
|
||||||
|
totalEnergyDrawnFromBatteryWh = 0; // Explicitly reset these for clarity
|
||||||
|
totalEnergyChargedToBatteryWh = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (let i = 0; i < numberOfIntervals; i++) {
|
||||||
|
const currentIntervalStartSimTime = i * timePerIntervalHours;
|
||||||
|
let intervalDevicePowerW = 0;
|
||||||
|
const intervalMidPoint = currentIntervalStartSimTime + timePerIntervalHours / 2;
|
||||||
|
let deviceIsActiveInThisInterval = false;
|
||||||
|
|
||||||
|
if (currentIntervalStartSimTime < totalHours) {
|
||||||
|
for (let dayOffset = 0; ; dayOffset++) { // Loop through potential days
|
||||||
|
const dayStartTimeOffset = dayOffset * 24;
|
||||||
|
const currentDayDeviceStart = dayStartTimeOffset + deviceStartHour;
|
||||||
|
const currentDayDeviceEnd = dayStartTimeOffset + deviceStartHour + deviceOpHours;
|
||||||
|
|
||||||
|
if (currentDayDeviceStart >= totalHours) break; // This device cycle starts after simulation ends
|
||||||
|
|
||||||
|
if (intervalMidPoint >= currentDayDeviceStart && intervalMidPoint < currentDayDeviceEnd) {
|
||||||
|
deviceIsActiveInThisInterval = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (currentDayDeviceEnd > currentIntervalStartSimTime + totalHours + 24) break; // Optimization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceIsActiveInThisInterval) {
|
||||||
|
const deviceRandomFluctuation = (Math.random() * 2 - 1) * deviceFluctuationPercentage;
|
||||||
|
intervalDevicePowerW = powerDeviceAvgW * (1 + deviceRandomFluctuation);
|
||||||
|
intervalDevicePowerW = Math.max(0, intervalDevicePowerW);
|
||||||
|
}
|
||||||
|
const actualIntervalDeviceEnergyWh = intervalDevicePowerW * timePerIntervalHours;
|
||||||
|
totalDeviceEnergyWh += actualIntervalDeviceEnergyWh;
|
||||||
|
|
||||||
|
const intervalPvPowerW_final = usePv ? pvProductionData_scaled[i] : 0;
|
||||||
|
const intervalPvEnergyWh_final = intervalPvPowerW_final * timePerIntervalHours;
|
||||||
|
|
||||||
|
let energyFromGridThisInterval = 0, energyFromBatteryThisInterval = 0, energyToBatteryThisInterval = 0;
|
||||||
|
let netAfterPv = actualIntervalDeviceEnergyWh - intervalPvEnergyWh_final;
|
||||||
|
|
||||||
|
if (useBattery) {
|
||||||
|
if (netAfterPv > 0) {
|
||||||
|
const canDrawFromBattery = Math.min(netAfterPv, currentBatteryEnergyWh);
|
||||||
|
energyFromBatteryThisInterval = canDrawFromBattery;
|
||||||
|
currentBatteryEnergyWh -= canDrawFromBattery;
|
||||||
|
totalEnergyDrawnFromBatteryWh += canDrawFromBattery;
|
||||||
|
energyFromGridThisInterval = netAfterPv - canDrawFromBattery;
|
||||||
|
} else {
|
||||||
|
const surplusPv = Math.abs(netAfterPv);
|
||||||
|
const canChargeToBattery = Math.min(surplusPv, batteryMaxEnergyWh - currentBatteryEnergyWh);
|
||||||
|
energyToBatteryThisInterval = canChargeToBattery;
|
||||||
|
currentBatteryEnergyWh += canChargeToBattery;
|
||||||
|
totalEnergyChargedToBatteryWh += canChargeToBattery;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (netAfterPv > 0) energyFromGridThisInterval = netAfterPv;
|
||||||
|
}
|
||||||
|
|
||||||
|
energyFromGridThisInterval = Math.max(0, energyFromGridThisInterval);
|
||||||
|
totalGridEnergyWh += energyFromGridThisInterval;
|
||||||
|
|
||||||
|
timeLabels.push((currentIntervalStartSimTime).toFixed(1) + 'h');
|
||||||
|
deviceConsumptionData.push(intervalDevicePowerW.toFixed(1));
|
||||||
|
if(useBattery) batteryChargeData.push((currentBatteryEnergyWh / batteryMaxEnergyWh * 100).toFixed(1));
|
||||||
|
gridConsumptionData.push((energyFromGridThisInterval / timePerIntervalHours).toFixed(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const finalTotalCost = (totalGridEnergyWh / 1000) * costPerKwh;
|
||||||
|
|
||||||
|
energyConsumedDeviceOutput.textContent = formatEnergy(totalDeviceEnergyWh);
|
||||||
|
energyFromPvOutput.textContent = usePv ? formatEnergy(finalTotalPvEnergyWh) : '--';
|
||||||
|
energyFromBatteryOutput.textContent = useBattery ? formatEnergy(totalEnergyDrawnFromBatteryWh) : '--';
|
||||||
|
energyToBatteryOutput.textContent = useBattery ? formatEnergy(totalEnergyChargedToBatteryWh) : '--';
|
||||||
|
energyFromGridOutput.textContent = formatEnergy(totalGridEnergyWh);
|
||||||
|
totalCostOutput.textContent = finalTotalCost.toLocaleString(currentLang === 'it' ? 'it-IT' : 'en-US', { style: 'currency', currency: 'EUR' });
|
||||||
|
|
||||||
|
if (consumptionChart) consumptionChart.destroy();
|
||||||
|
const datasets = [
|
||||||
|
{ label: t.chartLabelDeviceConsumption, data: deviceConsumptionData, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.1)', tension: 0.3, fill: true, yAxisID: 'yPower' },
|
||||||
|
{ label: t.chartLabelGridDraw, data: gridConsumptionData, borderColor: 'rgba(255, 159, 64, 1)', backgroundColor: 'rgba(255, 159, 64, 0.1)', tension: 0.3, fill: true, yAxisID: 'yPower' }
|
||||||
|
];
|
||||||
|
if (usePv) {
|
||||||
|
datasets.push({ label: t.chartLabelPvProduction, data: pvProductionData_scaled.map(p => p.toFixed(1)), borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.1)', tension: 0.3, fill: true, yAxisID: 'yPower' });
|
||||||
|
datasets.push({ label: t.chartLabelAvgPvProduction, data: Array(numberOfIntervals).fill((avgDailyPvEnergyWh_target_adjusted/24).toFixed(1)), borderColor: 'rgba(75, 192, 192, 0.5)', borderDash: [3, 3], tension: 0.1, fill: false, pointRadius: 0, yAxisID: 'yPower' });
|
||||||
|
}
|
||||||
|
if (useBattery) {
|
||||||
|
datasets.push({ label: t.chartLabelBatteryCharge, data: batteryChargeData, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.3, fill: false, yAxisID: 'yBattery' });
|
||||||
|
}
|
||||||
|
|
||||||
|
consumptionChart = new Chart(consumptionChartCanvas, {
|
||||||
|
type: 'line', data: { labels: timeLabels, datasets: datasets },
|
||||||
|
options: {
|
||||||
|
responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, stacked: false,
|
||||||
|
scales: {
|
||||||
|
yPower: { type: 'linear', display: true, position: 'left', title: { display: true, text: t.chartAxisPower, font: {size: 12}, color: '#4a5568' }, grid: { color: '#e2e8f0' }, ticks: { color: '#718096'} },
|
||||||
|
yBattery: { type: 'linear', display: useBattery, position: 'right', min: 0, max: 100, title: { display: true, text: t.chartAxisBattery, font: {size: 12}, color: '#4a5568' }, grid: { drawOnChartArea: false }, ticks: { color: '#718096'} },
|
||||||
|
x: { title: { display: true, text: t.chartAxisDuration, font: {size: 14, weight: '500'}, color: '#4a5568' }, grid: { display: false }, ticks: { color: '#718096', maxRotation: 45, autoSkip: true, maxTicksLimit: 24 } }
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top', labels: { font: {size: 10}, color: '#4a5568'} },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
let label = context.dataset.label || '';
|
||||||
|
if (label) label += ': ';
|
||||||
|
if (context.parsed.y !== null) {
|
||||||
|
const unit = context.dataset.yAxisID === 'yBattery' ? ' %' : ' W';
|
||||||
|
label += parseFloat(context.parsed.y).toLocaleString(currentLang === 'it' ? 'it-IT' : 'en-US', {minimumFractionDigits:1, maximumFractionDigits:1}) + unit;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: { duration: 500, easing: 'easeInOutQuart' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateUI(currentLang);
|
||||||
|
chartMessageP.classList.remove('hidden');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
770
index.html
770
index.html
@@ -2,709 +2,87 @@
|
|||||||
<html lang="it">
|
<html lang="it">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>Energy Calculator - Home</title>
|
||||||
<title>Calcolatore Costi Elettricità Avanzato</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'Inter', sans-serif; background-color: #f0f4f8; }
|
body {
|
||||||
.card { background-color: white; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); padding: 1.5rem; margin-bottom: 1.5rem; }
|
font-family: Arial, sans-serif;
|
||||||
.input-label { font-size: 0.875rem; font-weight: 500; color: #4a5568; margin-bottom: 0.5rem; }
|
background: #f4f6f8;
|
||||||
.input-field, .select-field { width: 100%; padding: 0.75rem 1rem; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 1rem; transition: border-color 0.2s ease-in-out; background-color: white; }
|
margin: 0;
|
||||||
.input-field:focus, .select-field:focus { outline: none; border-color: #4299e1; box-shadow: 0 0 0 3px rgba(66,153,225,0.5); }
|
padding: 0;
|
||||||
.btn-primary { background-color: #4299e1; color: white; font-weight: 600; padding: 0.75rem 1.5rem; border-radius: 8px; transition: background-color 0.2s ease-in-out; cursor: pointer; border: none; }
|
display: flex;
|
||||||
.btn-primary:hover { background-color: #3182ce; }
|
flex-direction: column;
|
||||||
.output-value { font-size: 1.25rem; font-weight: 700; color: #2c5282; }
|
min-height: 100vh;
|
||||||
.output-label { font-size: 0.8rem; color: #718096; }
|
}
|
||||||
#errorMessage { color: #e53e3e; background-color: #fed7d7; border: 1px solid #f56565; padding: 0.75rem; border-radius: 8px; margin-top: 1rem; text-align: center; }
|
header {
|
||||||
.optional-section { border-top: 1px dashed #cbd5e0; margin-top: 1rem; padding-top: 1rem; }
|
background: #2d6cdf;
|
||||||
.checkbox-label { font-size: 0.9rem; font-weight: 500; color: #4a5568; margin-left: 0.5rem; }
|
color: #fff;
|
||||||
.lang-selector { padding: 0.3rem 0.6rem; font-size: 0.8rem; margin-left: 0.5rem; border-radius: 6px; }
|
padding: 1.5rem 0;
|
||||||
.device-schedule-section { border-top: 1px solid #e2e8f0; margin-top: 1rem; padding-top: 1rem; }
|
text-align: center;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.card-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
padding: 2rem 2.5rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(45,108,223,0.15);
|
||||||
|
}
|
||||||
|
.card a {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.7rem 1.5rem;
|
||||||
|
background: #2d6cdf;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.card a:hover {
|
||||||
|
background: #1b4e9b;
|
||||||
|
}
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.card-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex flex-col items-center justify-center p-4 sm:p-6">
|
<body>
|
||||||
|
<header>
|
||||||
<div class="w-full max-w-4xl">
|
<h1>Energy Calculator</h1>
|
||||||
<header class="text-center mb-6 relative">
|
<p>Benvenuto! Scegli una funzione per iniziare:</p>
|
||||||
<div class="absolute top-0 right-0 mt-2 mr-2">
|
|
||||||
<select id="languageSelector" class="select-field lang-selector">
|
|
||||||
<option value="it">Italiano</option>
|
|
||||||
<option value="en">English</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<h1 id="mainTitle" class="text-3xl sm:text-4xl font-bold text-gray-800 pt-8">Calcolo Avanzato Consumi e Costi Elettricità</h1>
|
|
||||||
<p id="subTitle" class="text-gray-600 mt-2">Includi fotovoltaico e batteria per una stima più completa.</p>
|
|
||||||
</header>
|
</header>
|
||||||
|
<main>
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div class="card-container">
|
||||||
<!-- Colonna Input -->
|
|
||||||
<div class="lg:col-span-1 card">
|
|
||||||
<h2 id="dataInputTitle" class="text-xl font-semibold text-gray-700 mb-6">Inserisci i Dati</h2>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label for="power" id="labelDevicePower" class="input-label block">Consumo Medio Dispositivo (Watt)</label>
|
|
||||||
<input type="number" id="power" class="input-field" placeholder="es. 100" value="100">
|
|
||||||
</div>
|
|
||||||
<div class="device-schedule-section">
|
|
||||||
<label for="deviceStartHour" id="labelDeviceStartHour" class="input-label block">Ora Inizio Funzionamento Dispositivo</label>
|
|
||||||
<input type="number" id="deviceStartHour" class="input-field" placeholder="0-23 (es. 8)" value="8" min="0" max="23">
|
|
||||||
|
|
||||||
<label for="deviceOperatingHours" id="labelDeviceOpHours" class="input-label block mt-2">Durata Funzionamento Dispositivo (Ore)</label>
|
|
||||||
<input type="number" id="deviceOperatingHours" class="input-field" placeholder="1-24 (es. 10)" value="10" min="1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="cost" id="labelGridCost" class="input-label block mt-3">Costo Energia da Rete (€/kWh)</label>
|
|
||||||
<input type="number" id="cost" class="input-field" placeholder="es. 0,25" value="0.25" step="0.01">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="time" id="labelSimDuration" class="input-label block">Durata Simulazione (Ore)</label>
|
|
||||||
<input type="number" id="time" class="input-field" placeholder="es. 24" value="24" min="1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sezione Fotovoltaico -->
|
|
||||||
<div class="optional-section">
|
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<input type="checkbox" id="includePv" class="rounded">
|
|
||||||
<label for="includePv" id="labelIncludePv" class="checkbox-label">Includi Impianto Fotovoltaico</label>
|
|
||||||
</div>
|
|
||||||
<div id="pvInputs" class="hidden space-y-3 pl-2">
|
|
||||||
<div>
|
|
||||||
<label for="pvPeakPower" id="labelPvPeakPower" class="input-label block">Potenza Picco Impianto FV (kWp)</label>
|
|
||||||
<input type="number" id="pvPeakPower" class="input-field" placeholder="es. 3" value="3" step="0.1">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="geographicZone" id="labelGeoZone" class="input-label block">Paese / Regione</label>
|
|
||||||
<select id="geographicZone" class="select-field">
|
|
||||||
<!-- Options will be populated by JS -->
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="season" id="labelSeason" class="input-label block">Stagione</label>
|
|
||||||
<select id="season" class="select-field">
|
|
||||||
<!-- Options will be populated by JS -->
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="dayType" id="labelDayType" class="input-label block">Tipo di Giornata</label>
|
|
||||||
<select id="dayType" class="select-field">
|
|
||||||
<!-- Options will be populated by JS -->
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center mt-1">
|
|
||||||
<input type="checkbox" id="useSimulatedWeather" class="rounded">
|
|
||||||
<label for="useSimulatedWeather" id="labelUseSimulatedWeather" class="checkbox-label text-xs">Usa Meteo Attuale (Simulato)</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sezione Batteria -->
|
|
||||||
<div class="optional-section">
|
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<input type="checkbox" id="includeBattery" class="rounded">
|
|
||||||
<label for="includeBattery" id="labelIncludeBattery" class="checkbox-label">Includi Batteria di Accumulo</label>
|
|
||||||
</div>
|
|
||||||
<div id="batteryInputs" class="hidden space-y-3 pl-2">
|
|
||||||
<div>
|
|
||||||
<label for="batteryCapacity" id="labelBatteryCapacity" class="input-label block">Capacità Batteria (kWh)</label>
|
|
||||||
<input type="number" id="batteryCapacity" class="input-field" placeholder="es. 5" value="5" step="0.1">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="initialBatteryCharge" id="labelInitialBattery" class="input-label block">Carica Iniziale Batteria (%)</label>
|
|
||||||
<input type="number" id="initialBatteryCharge" class="input-field" placeholder="es. 50" value="50" min="0" max="100">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="calculateBtn" class="btn-primary w-full mt-6">Calcola</button>
|
|
||||||
<div id="errorMessage" class="hidden mt-4"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Colonna Output e Grafico -->
|
|
||||||
<div class="lg:col-span-2">
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 id="resultsTitle" class="text-xl font-semibold text-gray-700 mb-4">Risultati Stimati</h2>
|
<h2>Calcolatore Risparmi</h2>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-4">
|
<p>Scopri quanto puoi risparmiare con il nostro calcolatore.</p>
|
||||||
<div class="text-center p-2 bg-gray-50 rounded-lg">
|
<a href="savings-calculator.html">Vai al Calcolatore Risparmi</a>
|
||||||
<p id="energyConsumedDevice" class="output-value">--</p>
|
|
||||||
<p id="labelDeviceConsumption" class="output-label">Consumo Dispositivo</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center p-2 bg-green-50 rounded-lg">
|
<div class="card">
|
||||||
<p id="energyFromPv" class="output-value">--</p>
|
<h2>Calcolatore Costi</h2>
|
||||||
<p id="labelEnergyFromPv" class="output-label">Energia da FV</p>
|
<p>Calcola i costi energetici in modo semplice e veloce.</p>
|
||||||
</div>
|
<a href="cost-calculator.html">Vai al Calcolatore Costi</a>
|
||||||
<div class="text-center p-2 bg-blue-50 rounded-lg">
|
|
||||||
<p id="energyFromBattery" class="output-value">--</p>
|
|
||||||
<p id="labelEnergyFromBattery" class="output-label">Energia da Batteria</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center p-2 bg-orange-50 rounded-lg">
|
|
||||||
<p id="energyToBattery" class="output-value">--</p>
|
|
||||||
<p id="labelEnergyToBattery" class="output-label">Energia a Batteria</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center p-2 bg-red-50 rounded-lg">
|
|
||||||
<p id="energyFromGrid" class="output-value">--</p>
|
|
||||||
<p id="labelEnergyFromGrid" class="output-label">Prelievo da Rete</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center p-2 bg-purple-50 rounded-lg">
|
|
||||||
<p id="totalCost" class="output-value">--</p>
|
|
||||||
<p id="labelTotalGridCost" class="output-label">Costo Totale Rete</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="simulationNote" class="text-xs text-gray-500 mt-3 text-center">
|
</main>
|
||||||
Nota: il consumo del dispositivo avviene solo nelle ore specificate e presenta fluttuazioni (5%). La produzione FV è stimata (con fluttuazioni dinamiche basate su zona/stagione/tipo giornata) considerando un ciclo giorno/notte.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mt-6 w-full">
|
|
||||||
<h2 id="chartTitle" class="text-xl font-semibold text-gray-700 mb-4 text-center">Andamento Energetico Simulato</h2>
|
|
||||||
<div class="relative h-72 sm:h-96">
|
|
||||||
<canvas id="consumptionChart"></canvas>
|
|
||||||
</div>
|
|
||||||
<p id="chartMessage" class="text-sm text-gray-500 mt-4 text-center">Premi "Calcola" per generare il grafico.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="text-center mt-8 text-sm text-gray-500">
|
|
||||||
<p id="footerText">© 2024 Calcolatore Energetico Avanzato. Solo a scopo illustrativo.</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// --- DOM Elements ---
|
|
||||||
const languageSelector = document.getElementById('languageSelector');
|
|
||||||
const powerInput = document.getElementById('power');
|
|
||||||
const deviceStartHourInput = document.getElementById('deviceStartHour');
|
|
||||||
const deviceOperatingHoursInput = document.getElementById('deviceOperatingHours');
|
|
||||||
const costInput = document.getElementById('cost');
|
|
||||||
const timeInput = document.getElementById('time');
|
|
||||||
const includePvCheckbox = document.getElementById('includePv');
|
|
||||||
const pvInputsDiv = document.getElementById('pvInputs');
|
|
||||||
const pvPeakPowerInput = document.getElementById('pvPeakPower');
|
|
||||||
const seasonSelect = document.getElementById('season');
|
|
||||||
const geographicZoneSelect = document.getElementById('geographicZone');
|
|
||||||
const dayTypeSelect = document.getElementById('dayType');
|
|
||||||
const useSimulatedWeatherCheckbox = document.getElementById('useSimulatedWeather');
|
|
||||||
const includeBatteryCheckbox = document.getElementById('includeBattery');
|
|
||||||
const batteryInputsDiv = document.getElementById('batteryInputs');
|
|
||||||
const batteryCapacityInput = document.getElementById('batteryCapacity');
|
|
||||||
const initialBatteryChargeInput = document.getElementById('initialBatteryCharge');
|
|
||||||
const calculateBtn = document.getElementById('calculateBtn');
|
|
||||||
const energyConsumedDeviceOutput = document.getElementById('energyConsumedDevice');
|
|
||||||
const energyFromPvOutput = document.getElementById('energyFromPv');
|
|
||||||
const energyFromBatteryOutput = document.getElementById('energyFromBattery');
|
|
||||||
const energyToBatteryOutput = document.getElementById('energyToBattery');
|
|
||||||
const energyFromGridOutput = document.getElementById('energyFromGrid');
|
|
||||||
const totalCostOutput = document.getElementById('totalCost');
|
|
||||||
const consumptionChartCanvas = document.getElementById('consumptionChart');
|
|
||||||
const errorMessageDiv = document.getElementById('errorMessage');
|
|
||||||
const chartMessageP = document.getElementById('chartMessage');
|
|
||||||
|
|
||||||
let consumptionChart = null;
|
|
||||||
let currentLang = 'it';
|
|
||||||
|
|
||||||
const translations = {
|
|
||||||
it: {
|
|
||||||
docTitle: "Calcolatore Costi Elettricità Avanzato",
|
|
||||||
mainTitle: "Calcolo Avanzato Consumi e Costi Elettricità",
|
|
||||||
subTitle: "Includi fotovoltaico e batteria per una stima più completa.",
|
|
||||||
dataInputTitle: "Inserisci i Dati",
|
|
||||||
labelDevicePower: "Consumo Medio Dispositivo (Watt, durante funzionamento)",
|
|
||||||
labelDeviceStartHour: "Ora Inizio Funzionamento Dispositivo (0-23)",
|
|
||||||
labelDeviceOpHours: "Durata Funzionamento Dispositivo (Ore)",
|
|
||||||
labelGridCost: "Costo Energia da Rete (€/kWh)",
|
|
||||||
labelSimDuration: "Durata Intera Simulazione (Ore)",
|
|
||||||
labelIncludePv: "Includi Impianto Fotovoltaico",
|
|
||||||
labelPvPeakPower: "Potenza Picco Impianto FV (kWp)",
|
|
||||||
labelSeason: "Stagione",
|
|
||||||
labelGeoZone: "Paese / Regione",
|
|
||||||
labelDayType: "Tipo di Giornata",
|
|
||||||
labelUseSimulatedWeather: "Usa Meteo Attuale (Simulato)",
|
|
||||||
labelIncludeBattery: "Includi Batteria di Accumulo",
|
|
||||||
labelBatteryCapacity: "Capacità Batteria (kWh)",
|
|
||||||
labelInitialBattery: "Carica Iniziale Batteria (%)",
|
|
||||||
calculateBtn: "Calcola",
|
|
||||||
resultsTitle: "Risultati Stimati",
|
|
||||||
labelDeviceConsumption: "Consumo Dispositivo",
|
|
||||||
labelEnergyFromPv: "Energia da FV",
|
|
||||||
labelEnergyFromBattery: "Energia da Batteria",
|
|
||||||
labelEnergyToBattery: "Energia a Batteria",
|
|
||||||
labelEnergyFromGrid: "Prelievo da Rete",
|
|
||||||
labelTotalGridCost: "Costo Totale Rete",
|
|
||||||
simulationNote: "Nota: il consumo del dispositivo avviene solo nelle ore specificate e presenta fluttuazioni (5%). La produzione FV è stimata (con fluttuazioni dinamiche basate su zona/stagione/tipo giornata) considerando un ciclo giorno/notte.",
|
|
||||||
chartTitle: "Andamento Energetico Simulato",
|
|
||||||
chartMessage: 'Premi "Calcola" per generare il grafico.',
|
|
||||||
footerText: "© 2024 Calcolatore Energetico Avanzato. Solo a scopo illustrativo.",
|
|
||||||
errorDevicePower: "Inserisci una potenza valida per il dispositivo (>0 Watt).",
|
|
||||||
errorDeviceStartHour: "Ora inizio funzionamento dispositivo non valida (0-23).",
|
|
||||||
errorDeviceOpHours: "Durata funzionamento dispositivo non valida (>0 ore).",
|
|
||||||
errorGridCost: "Inserisci un costo per kWh valido (>=0).",
|
|
||||||
errorSimDuration: "Inserisci una durata simulazione valida (>0 ore).",
|
|
||||||
errorPvPeakPower: "Inserisci una potenza di picco FV valida (>0 kWp).",
|
|
||||||
errorPvCombination: "Combinazione zona/stagione/tipo giornata non valida per FV.",
|
|
||||||
errorBatteryCapacity: "Inserisci una capacità batteria valida (>0 kWh).",
|
|
||||||
errorInitialBattery: "Inserisci una carica iniziale batteria valida (0-100%).",
|
|
||||||
chartLabelDeviceConsumption: "Consumo Dispositivo (W)",
|
|
||||||
chartLabelAvgDevicePower: "Potenza Media Dispositivo (W, se attivo)",
|
|
||||||
chartLabelGridDraw: "Prelievo da Rete (W)",
|
|
||||||
chartLabelPvProduction: "Produzione FV (W)",
|
|
||||||
chartLabelAvgPvProduction: "Produzione Media FV Attesa (W, su 24h)",
|
|
||||||
chartLabelBatteryCharge: "Carica Batteria (%)",
|
|
||||||
chartAxisPower: "Potenza (Watt)",
|
|
||||||
chartAxisBattery: "Carica Batteria (%)",
|
|
||||||
chartAxisDuration: "Durata (Ore)",
|
|
||||||
seasons: { inverno: "Inverno", mezzaStagione: "Primavera/Autunno", estate: "Estate" },
|
|
||||||
dayTypes: { sunny: "Soleggiata", partlyCloudy: "Parz. Nuvolosa", cloudy: "Nuvolosa/Pioggia" },
|
|
||||||
geoZones: {
|
|
||||||
it_north: "Italia - Nord", it_center: "Italia - Centro", it_south: "Italia - Sud/Isole",
|
|
||||||
de_north: "Germania - Nord", de_south: "Germania - Sud",
|
|
||||||
es_south: "Spagna - Sud", fr_north: "Francia - Nord",
|
|
||||||
uk_south: "Regno Unito - Sud",
|
|
||||||
usa_ca: "USA - California", usa_ny: "USA - New York",
|
|
||||||
eq_generic: "Zona Equatoriale Generica"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
docTitle: "Advanced Electricity Cost Calculator",
|
|
||||||
mainTitle: "Advanced Electricity Consumption and Cost Calculator",
|
|
||||||
subTitle: "Include photovoltaics and battery for a more complete estimate.",
|
|
||||||
dataInputTitle: "Enter Your Data",
|
|
||||||
labelDevicePower: "Average Device Consumption (Watts, when ON)",
|
|
||||||
labelDeviceStartHour: "Device Operating Start Hour (0-23)",
|
|
||||||
labelDeviceOpHours: "Device Operating Duration (Hours)",
|
|
||||||
labelGridCost: "Grid Energy Cost (€/kWh)",
|
|
||||||
labelSimDuration: "Total Simulation Duration (Hours)",
|
|
||||||
labelIncludePv: "Include Photovoltaic System",
|
|
||||||
labelPvPeakPower: "PV System Peak Power (kWp)",
|
|
||||||
labelSeason: "Season",
|
|
||||||
labelGeoZone: "Country / Region",
|
|
||||||
labelDayType: "Day Type",
|
|
||||||
labelUseSimulatedWeather: "Use Current Weather (Simulated)",
|
|
||||||
labelIncludeBattery: "Include Storage Battery",
|
|
||||||
labelBatteryCapacity: "Battery Capacity (kWh)",
|
|
||||||
labelInitialBattery: "Initial Battery Charge (%)",
|
|
||||||
calculateBtn: "Calculate",
|
|
||||||
resultsTitle: "Estimated Results",
|
|
||||||
labelDeviceConsumption: "Device Consumption",
|
|
||||||
labelEnergyFromPv: "Energy from PV",
|
|
||||||
labelEnergyFromBattery: "Energy from Battery",
|
|
||||||
labelEnergyToBattery: "Energy to Battery",
|
|
||||||
labelEnergyFromGrid: "Grid Draw",
|
|
||||||
labelTotalGridCost: "Total Grid Cost",
|
|
||||||
simulationNote: "Note: Device consumption occurs only during specified hours and includes fluctuations (5%). PV production is estimated (with dynamic fluctuations based on zone/season/day type) considering a day/night cycle.",
|
|
||||||
chartTitle: "Simulated Energy Flow",
|
|
||||||
chartMessage: 'Press "Calculate" to generate the chart.',
|
|
||||||
footerText: "© 2024 Advanced Energy Calculator. For illustrative purposes only.",
|
|
||||||
errorDevicePower: "Enter a valid device power (>0 Watts).",
|
|
||||||
errorDeviceStartHour: "Invalid device start hour (0-23).",
|
|
||||||
errorDeviceOpHours: "Invalid device operating duration (>0 hours).",
|
|
||||||
errorGridCost: "Enter a valid cost per kWh (>=0).",
|
|
||||||
errorSimDuration: "Enter a valid simulation duration (>0 hours).",
|
|
||||||
errorPvPeakPower: "Enter a valid PV peak power (>0 kWp).",
|
|
||||||
errorPvCombination: "Invalid zone/season/day type combination for PV.",
|
|
||||||
errorBatteryCapacity: "Enter a valid battery capacity (>0 kWh).",
|
|
||||||
errorInitialBattery: "Enter a valid initial battery charge (0-100%).",
|
|
||||||
chartLabelDeviceConsumption: "Device Consumption (W)",
|
|
||||||
chartLabelAvgDevicePower: "Avg Device Power (W, if ON)",
|
|
||||||
chartLabelGridDraw: "Grid Draw (W)",
|
|
||||||
chartLabelPvProduction: "PV Production (W)",
|
|
||||||
chartLabelAvgPvProduction: "Avg Expected PV Production (W, 24h avg)",
|
|
||||||
chartLabelBatteryCharge: "Battery Charge (%)",
|
|
||||||
chartAxisPower: "Power (Watts)",
|
|
||||||
chartAxisBattery: "Battery Charge (%)",
|
|
||||||
chartAxisDuration: "Duration (Hours)",
|
|
||||||
seasons: { inverno: "Winter", mezzaStagione: "Spring/Autumn", estate: "Summer" },
|
|
||||||
dayTypes: { sunny: "Sunny", partlyCloudy: "Partly Cloudy", cloudy: "Cloudy/Rainy" },
|
|
||||||
geoZones: {
|
|
||||||
it_north: "Italy - North", it_center: "Italy - Center", it_south: "Italy - South/Islands",
|
|
||||||
de_north: "Germany - North", de_south: "Germany - South",
|
|
||||||
es_south: "Spain - South", fr_north: "France - North",
|
|
||||||
uk_south: "United Kingdom - South",
|
|
||||||
usa_ca: "USA - California", usa_ny: "USA - New York",
|
|
||||||
eq_generic: "Generic Equatorial Zone"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// kWh/kWp/day for a TYPICALLY SUNNY DAY
|
|
||||||
const yieldFactors = {
|
|
||||||
it_north: { inverno: 1.2, mezzaStagione: 3.0, estate: 4.8 },
|
|
||||||
it_center: { inverno: 1.8, mezzaStagione: 4.0, estate: 6.0 }, // User's reference
|
|
||||||
it_south: { inverno: 2.5, mezzaStagione: 4.8, estate: 7.0 },
|
|
||||||
de_north: { inverno: 0.8, mezzaStagione: 2.5, estate: 4.2 },
|
|
||||||
de_south: { inverno: 1.0, mezzaStagione: 3.0, estate: 4.8 },
|
|
||||||
es_south: { inverno: 2.8, mezzaStagione: 5.0, estate: 7.5 },
|
|
||||||
fr_north: { inverno: 1.0, mezzaStagione: 2.9, estate: 4.6 },
|
|
||||||
uk_south: { inverno: 0.9, mezzaStagione: 2.7, estate: 4.3 },
|
|
||||||
usa_ca: { inverno: 3.0, mezzaStagione: 5.5, estate: 7.8 },
|
|
||||||
usa_ny: { inverno: 1.5, mezzaStagione: 3.8, estate: 5.2 },
|
|
||||||
eq_generic:{ inverno: 4.2, mezzaStagione: 4.5, estate: 4.8 } // Less seasonal variation, more consistent
|
|
||||||
};
|
|
||||||
|
|
||||||
// Defines daylight window and reference hours for calculating a higher power base for fluctuations
|
|
||||||
const seasonalDaylightAndPowerRefHours = {
|
|
||||||
inverno: { start: 8, end: 17, refHoursForPowerCalc: 4 }, // Adjusted refHours
|
|
||||||
mezzaStagione: { start: 7, end: 19, refHoursForPowerCalc: 6 },
|
|
||||||
estate: { start: 6, end: 20, refHoursForPowerCalc: 8 } // Longer ref for summer
|
|
||||||
};
|
|
||||||
|
|
||||||
// +/- percentage range, relative to the curve generated for the day type
|
|
||||||
const pvBaseFluctuation = 0.10; // Base random noise on top of the shaped curve
|
|
||||||
const dayTypeModifiers = {
|
|
||||||
// Factor to multiply daily energy yield by, and base variability factor
|
|
||||||
sunny: { yieldFactor: 1.0, variabilityFactor: 0.15, shapeIntensity: 1.0 }, // Shape intensity for how "peaky"
|
|
||||||
partlyCloudy: { yieldFactor: 0.6, variabilityFactor: 0.40, shapeIntensity: 0.7 },
|
|
||||||
cloudy: { yieldFactor: 0.25, variabilityFactor: 0.65, shapeIntensity: 0.4 }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function updateUI(lang) {
|
|
||||||
currentLang = lang;
|
|
||||||
document.documentElement.lang = lang;
|
|
||||||
const t = translations[lang];
|
|
||||||
|
|
||||||
document.title = t.docTitle;
|
|
||||||
document.getElementById('mainTitle').textContent = t.mainTitle;
|
|
||||||
document.getElementById('subTitle').textContent = t.subTitle;
|
|
||||||
document.getElementById('dataInputTitle').textContent = t.dataInputTitle;
|
|
||||||
document.getElementById('labelDevicePower').textContent = t.labelDevicePower;
|
|
||||||
document.getElementById('labelDeviceStartHour').textContent = t.labelDeviceStartHour;
|
|
||||||
document.getElementById('labelDeviceOpHours').textContent = t.labelDeviceOpHours;
|
|
||||||
document.getElementById('labelGridCost').textContent = t.labelGridCost;
|
|
||||||
document.getElementById('labelSimDuration').textContent = t.labelSimDuration;
|
|
||||||
document.getElementById('labelIncludePv').textContent = t.labelIncludePv;
|
|
||||||
document.getElementById('labelPvPeakPower').textContent = t.labelPvPeakPower;
|
|
||||||
document.getElementById('labelSeason').textContent = t.labelSeason;
|
|
||||||
document.getElementById('labelGeoZone').textContent = t.labelGeoZone;
|
|
||||||
document.getElementById('labelDayType').textContent = t.labelDayType;
|
|
||||||
document.getElementById('labelUseSimulatedWeather').textContent = t.labelUseSimulatedWeather;
|
|
||||||
document.getElementById('labelIncludeBattery').textContent = t.labelIncludeBattery;
|
|
||||||
document.getElementById('labelBatteryCapacity').textContent = t.labelBatteryCapacity;
|
|
||||||
document.getElementById('labelInitialBattery').textContent = t.labelInitialBattery;
|
|
||||||
calculateBtn.textContent = t.calculateBtn;
|
|
||||||
document.getElementById('resultsTitle').textContent = t.resultsTitle;
|
|
||||||
document.getElementById('labelDeviceConsumption').textContent = t.labelDeviceConsumption;
|
|
||||||
document.getElementById('labelEnergyFromPv').textContent = t.labelEnergyFromPv;
|
|
||||||
document.getElementById('labelEnergyFromBattery').textContent = t.labelEnergyFromBattery;
|
|
||||||
document.getElementById('labelEnergyToBattery').textContent = t.labelEnergyToBattery;
|
|
||||||
document.getElementById('labelEnergyFromGrid').textContent = t.labelEnergyFromGrid;
|
|
||||||
document.getElementById('labelTotalGridCost').textContent = t.labelTotalGridCost;
|
|
||||||
document.getElementById('simulationNote').textContent = t.simulationNote;
|
|
||||||
document.getElementById('chartTitle').textContent = t.chartTitle;
|
|
||||||
chartMessageP.textContent = t.chartMessage;
|
|
||||||
document.getElementById('footerText').textContent = t.footerText;
|
|
||||||
|
|
||||||
seasonSelect.innerHTML = '';
|
|
||||||
for (const key in t.seasons) {
|
|
||||||
const option = document.createElement('option'); option.value = key; option.textContent = t.seasons[key];
|
|
||||||
if (key === 'mezzaStagione') option.selected = true; seasonSelect.appendChild(option);
|
|
||||||
}
|
|
||||||
geographicZoneSelect.innerHTML = '';
|
|
||||||
for (const key in t.geoZones) {
|
|
||||||
const option = document.createElement('option'); option.value = key; option.textContent = t.geoZones[key];
|
|
||||||
// Default to Italy Center if available, else first
|
|
||||||
if (key === 'it_center' || (geographicZoneSelect.options.length === 0 && key === Object.keys(t.geoZones)[0])) option.selected = true;
|
|
||||||
geographicZoneSelect.appendChild(option);
|
|
||||||
}
|
|
||||||
dayTypeSelect.innerHTML = '';
|
|
||||||
for (const key in t.dayTypes) {
|
|
||||||
const option = document.createElement('option'); option.value = key; option.textContent = t.dayTypes[key];
|
|
||||||
if (key === 'sunny') option.selected = true; dayTypeSelect.appendChild(option);
|
|
||||||
}
|
|
||||||
if (consumptionChart) { calculateBtn.click(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
languageSelector.addEventListener('change', (event) => updateUI(event.target.value));
|
|
||||||
includePvCheckbox.addEventListener('change', () => {
|
|
||||||
pvInputsDiv.classList.toggle('hidden', !includePvCheckbox.checked);
|
|
||||||
if (!includePvCheckbox.checked) {
|
|
||||||
includeBatteryCheckbox.checked = false;
|
|
||||||
batteryInputsDiv.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
includeBatteryCheckbox.addEventListener('change', () => batteryInputsDiv.classList.toggle('hidden', !includeBatteryCheckbox.checked));
|
|
||||||
|
|
||||||
function displayError(messageKey, lang = currentLang) {
|
|
||||||
errorMessageDiv.textContent = translations[lang][messageKey] || messageKey;
|
|
||||||
errorMessageDiv.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
function clearError() { errorMessageDiv.classList.add('hidden'); errorMessageDiv.textContent = ''; }
|
|
||||||
function formatEnergy(wh) {
|
|
||||||
if (wh === null || isNaN(wh)) return '--';
|
|
||||||
if (Math.abs(wh) < 1000) return wh.toFixed(1) + " Wh";
|
|
||||||
if (Math.abs(wh) < 1000000) return (wh / 1000).toFixed(2) + " kWh";
|
|
||||||
return (wh / 1000000).toFixed(3) + " MWh";
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateBtn.addEventListener('click', () => {
|
|
||||||
clearError();
|
|
||||||
chartMessageP.classList.add('hidden');
|
|
||||||
const t = translations[currentLang];
|
|
||||||
|
|
||||||
const powerDeviceAvgW = parseFloat(powerInput.value);
|
|
||||||
const deviceStartHour = parseInt(deviceStartHourInput.value, 10);
|
|
||||||
const deviceOpHours = parseFloat(deviceOperatingHoursInput.value);
|
|
||||||
const costPerKwh = parseFloat(costInput.value.replace(',', '.'));
|
|
||||||
const totalHours = parseFloat(timeInput.value);
|
|
||||||
|
|
||||||
let errors = [];
|
|
||||||
if (isNaN(powerDeviceAvgW) || powerDeviceAvgW <= 0) errors.push("errorDevicePower");
|
|
||||||
if (isNaN(deviceStartHour) || deviceStartHour < 0 || deviceStartHour > 23) errors.push("errorDeviceStartHour");
|
|
||||||
if (isNaN(deviceOpHours) || deviceOpHours <= 0 ) errors.push("errorDeviceOpHours");
|
|
||||||
if (isNaN(costPerKwh) || costPerKwh < 0) errors.push("errorGridCost");
|
|
||||||
if (isNaN(totalHours) || totalHours <= 0) errors.push("errorSimDuration");
|
|
||||||
|
|
||||||
const usePv = includePvCheckbox.checked;
|
|
||||||
let pvPeakPowerkWp = 0, season = seasonSelect.value, zone = geographicZoneSelect.value, dayType = dayTypeSelect.value;
|
|
||||||
let useSimWeather = useSimulatedWeatherCheckbox.checked;
|
|
||||||
|
|
||||||
let avgDailyPvEnergyWh_target_base = 0;
|
|
||||||
let pvDaylightConfig = seasonalDaylightAndPowerRefHours[season] || seasonalDaylightAndPowerRefHours.mezzaStagione;
|
|
||||||
let currentDayTypeModifier = dayTypeModifiers[dayType] || dayTypeModifiers.sunny;
|
|
||||||
|
|
||||||
if (usePv) {
|
|
||||||
pvPeakPowerkWp = parseFloat(pvPeakPowerInput.value);
|
|
||||||
if (isNaN(pvPeakPowerkWp) || pvPeakPowerkWp <= 0) errors.push("errorPvPeakPower");
|
|
||||||
|
|
||||||
const currentYieldFactors = yieldFactors[zone];
|
|
||||||
if (currentYieldFactors && currentYieldFactors[season] !== undefined) {
|
|
||||||
const selectedYieldFactor = currentYieldFactors[season];
|
|
||||||
avgDailyPvEnergyWh_target_base = pvPeakPowerkWp * 1000 * selectedYieldFactor; // Energy for a sunny day
|
|
||||||
} else {
|
|
||||||
errors.push("errorPvCombination");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const useBattery = includeBatteryCheckbox.checked;
|
|
||||||
let batteryCapacityKWh = 0, currentBatteryChargePercentage = 0;
|
|
||||||
let batteryMaxEnergyWh = 0, currentBatteryEnergyWh = 0;
|
|
||||||
if (useBattery) {
|
|
||||||
batteryCapacityKWh = parseFloat(batteryCapacityInput.value);
|
|
||||||
currentBatteryChargePercentage = parseFloat(initialBatteryChargeInput.value);
|
|
||||||
if (isNaN(batteryCapacityKWh) || batteryCapacityKWh <= 0) errors.push("errorBatteryCapacity");
|
|
||||||
if (isNaN(currentBatteryChargePercentage) || currentBatteryChargePercentage < 0 || currentBatteryChargePercentage > 100) errors.push("errorInitialBattery");
|
|
||||||
batteryMaxEnergyWh = batteryCapacityKWh * 1000;
|
|
||||||
currentBatteryEnergyWh = batteryMaxEnergyWh * (currentBatteryChargePercentage / 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) { displayError(errors[0]); return; }
|
|
||||||
|
|
||||||
const numberOfIntervals = Math.max(Math.ceil(totalHours * 4), 96); // Higher resolution for PV curve
|
|
||||||
const timePerIntervalHours = totalHours / numberOfIntervals;
|
|
||||||
const deviceFluctuationPercentage = 0.05;
|
|
||||||
|
|
||||||
// Apply day type modifier to the target daily energy
|
|
||||||
let avgDailyPvEnergyWh_target_adjusted = avgDailyPvEnergyWh_target_base * currentDayTypeModifier.yieldFactor;
|
|
||||||
|
|
||||||
// Apply simulated weather modifier
|
|
||||||
if (usePv && useSimWeather) {
|
|
||||||
const weatherModifier = 0.8 + Math.random() * 0.4; // Random factor between 0.8 (-20%) and 1.2 (+20%)
|
|
||||||
avgDailyPvEnergyWh_target_adjusted *= weatherModifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pvPowerBaseForCurveShape = 0;
|
|
||||||
if (usePv && pvDaylightConfig.refHoursForPowerCalc > 0) {
|
|
||||||
pvPowerBaseForCurveShape = avgDailyPvEnergyWh_target_adjusted / pvDaylightConfig.refHoursForPowerCalc;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let totalDeviceEnergyWh = 0, simulatedTotalPvEnergyWh_beforeScaling = 0, totalEnergyDrawnFromBatteryWh = 0;
|
|
||||||
let totalEnergyChargedToBatteryWh = 0, totalGridEnergyWh = 0;
|
|
||||||
|
|
||||||
const timeLabels = [], deviceConsumptionData = [], pvProductionData_raw = [], batteryChargeData = [], gridConsumptionData = [];
|
|
||||||
|
|
||||||
// --- Simulation Loop (Phase 1: Calculate Raw PV Production based on curve shape) ---
|
|
||||||
for (let i = 0; i < numberOfIntervals; i++) {
|
|
||||||
const currentIntervalStartSimTime = i * timePerIntervalHours;
|
|
||||||
const currentHourOfDay = currentIntervalStartSimTime % 24;
|
|
||||||
|
|
||||||
let intervalPvPowerW_shaped = 0;
|
|
||||||
if (usePv) {
|
|
||||||
if (currentHourOfDay >= pvDaylightConfig.start && currentHourOfDay < pvDaylightConfig.end) {
|
|
||||||
// Bell-curve shaping (simplified sinusoidal)
|
|
||||||
const hoursIntoDaylight = currentHourOfDay - pvDaylightConfig.start;
|
|
||||||
const daylightDuration = pvDaylightConfig.end - pvDaylightConfig.start;
|
|
||||||
const peakHour = pvDaylightConfig.start + daylightDuration / 2;
|
|
||||||
|
|
||||||
// More pronounced peak for sunny days, flatter for cloudy
|
|
||||||
let shapeFactor = Math.sin((Math.PI * hoursIntoDaylight) / daylightDuration);
|
|
||||||
shapeFactor = Math.pow(shapeFactor, currentDayTypeModifier.shapeIntensity); // Sharpen/flatten peak
|
|
||||||
|
|
||||||
intervalPvPowerW_shaped = pvPowerBaseForCurveShape * shapeFactor;
|
|
||||||
|
|
||||||
// Add random fluctuation based on day type variability
|
|
||||||
const randomFluctuation = (Math.random() * 2 - 1) * currentDayTypeModifier.variabilityFactor;
|
|
||||||
intervalPvPowerW_shaped *= (1 + randomFluctuation);
|
|
||||||
|
|
||||||
intervalPvPowerW_shaped = Math.max(0, Math.min(intervalPvPowerW_shaped, pvPeakPowerkWp * 1000));
|
|
||||||
} else {
|
|
||||||
intervalPvPowerW_shaped = 0; // Night time
|
|
||||||
}
|
|
||||||
const intervalPvEnergyWh = intervalPvPowerW_shaped * timePerIntervalHours;
|
|
||||||
simulatedTotalPvEnergyWh_beforeScaling += intervalPvEnergyWh;
|
|
||||||
pvProductionData_raw.push(intervalPvPowerW_shaped);
|
|
||||||
} else {
|
|
||||||
pvProductionData_raw.push(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- PV Energy Reconciliation (Phase 2) ---
|
|
||||||
let pvScalingFactor = 1;
|
|
||||||
if (usePv && simulatedTotalPvEnergyWh_beforeScaling > 0.001) { // Avoid division by zero or tiny numbers
|
|
||||||
const numberOfSimulatedDays = totalHours / 24;
|
|
||||||
const targetTotalPvEnergyWh_forSimDuration = avgDailyPvEnergyWh_target_adjusted * numberOfSimulatedDays;
|
|
||||||
pvScalingFactor = targetTotalPvEnergyWh_forSimDuration / simulatedTotalPvEnergyWh_beforeScaling;
|
|
||||||
} else if (usePv && avgDailyPvEnergyWh_target_adjusted > 0) { // Target was >0 but simulated was 0
|
|
||||||
pvScalingFactor = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const pvProductionData_scaled = pvProductionData_raw.map(p => Math.max(0, p * pvScalingFactor)); // Ensure no negative after scaling
|
|
||||||
let finalTotalPvEnergyWh = 0;
|
|
||||||
if(usePv) {
|
|
||||||
pvProductionData_scaled.forEach(power => {
|
|
||||||
finalTotalPvEnergyWh += power * timePerIntervalHours;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// --- Simulation Loop (Phase 3: Device, Battery, Grid with Scaled PV) ---
|
|
||||||
// Reset totals for the main simulation pass
|
|
||||||
totalDeviceEnergyWh = 0;
|
|
||||||
totalGridEnergyWh = 0;
|
|
||||||
// totalEnergyDrawnFromBatteryWh and totalEnergyChargedToBatteryWh are reset implicitly by not accumulating from prev runs
|
|
||||||
// currentBatteryEnergyWh is reset to initial for each calculation run
|
|
||||||
|
|
||||||
if (useBattery) { // Reset battery for this simulation run
|
|
||||||
currentBatteryEnergyWh = batteryMaxEnergyWh * (currentBatteryChargePercentage / 100);
|
|
||||||
totalEnergyDrawnFromBatteryWh = 0; // Explicitly reset these for clarity
|
|
||||||
totalEnergyChargedToBatteryWh = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for (let i = 0; i < numberOfIntervals; i++) {
|
|
||||||
const currentIntervalStartSimTime = i * timePerIntervalHours;
|
|
||||||
let intervalDevicePowerW = 0;
|
|
||||||
const intervalMidPoint = currentIntervalStartSimTime + timePerIntervalHours / 2;
|
|
||||||
let deviceIsActiveInThisInterval = false;
|
|
||||||
|
|
||||||
if (currentIntervalStartSimTime < totalHours) {
|
|
||||||
for (let dayOffset = 0; ; dayOffset++) { // Loop through potential days
|
|
||||||
const dayStartTimeOffset = dayOffset * 24;
|
|
||||||
const currentDayDeviceStart = dayStartTimeOffset + deviceStartHour;
|
|
||||||
const currentDayDeviceEnd = dayStartTimeOffset + deviceStartHour + deviceOpHours;
|
|
||||||
|
|
||||||
if (currentDayDeviceStart >= totalHours) break; // This device cycle starts after simulation ends
|
|
||||||
|
|
||||||
if (intervalMidPoint >= currentDayDeviceStart && intervalMidPoint < currentDayDeviceEnd) {
|
|
||||||
deviceIsActiveInThisInterval = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (currentDayDeviceEnd > currentIntervalStartSimTime + totalHours + 24) break; // Optimization
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceIsActiveInThisInterval) {
|
|
||||||
const deviceRandomFluctuation = (Math.random() * 2 - 1) * deviceFluctuationPercentage;
|
|
||||||
intervalDevicePowerW = powerDeviceAvgW * (1 + deviceRandomFluctuation);
|
|
||||||
intervalDevicePowerW = Math.max(0, intervalDevicePowerW);
|
|
||||||
}
|
|
||||||
const actualIntervalDeviceEnergyWh = intervalDevicePowerW * timePerIntervalHours;
|
|
||||||
totalDeviceEnergyWh += actualIntervalDeviceEnergyWh;
|
|
||||||
|
|
||||||
const intervalPvPowerW_final = usePv ? pvProductionData_scaled[i] : 0;
|
|
||||||
const intervalPvEnergyWh_final = intervalPvPowerW_final * timePerIntervalHours;
|
|
||||||
|
|
||||||
let energyFromGridThisInterval = 0, energyFromBatteryThisInterval = 0, energyToBatteryThisInterval = 0;
|
|
||||||
let netAfterPv = actualIntervalDeviceEnergyWh - intervalPvEnergyWh_final;
|
|
||||||
|
|
||||||
if (useBattery) {
|
|
||||||
if (netAfterPv > 0) {
|
|
||||||
const canDrawFromBattery = Math.min(netAfterPv, currentBatteryEnergyWh);
|
|
||||||
energyFromBatteryThisInterval = canDrawFromBattery;
|
|
||||||
currentBatteryEnergyWh -= canDrawFromBattery;
|
|
||||||
totalEnergyDrawnFromBatteryWh += canDrawFromBattery;
|
|
||||||
energyFromGridThisInterval = netAfterPv - canDrawFromBattery;
|
|
||||||
} else {
|
|
||||||
const surplusPv = Math.abs(netAfterPv);
|
|
||||||
const canChargeToBattery = Math.min(surplusPv, batteryMaxEnergyWh - currentBatteryEnergyWh);
|
|
||||||
energyToBatteryThisInterval = canChargeToBattery;
|
|
||||||
currentBatteryEnergyWh += canChargeToBattery;
|
|
||||||
totalEnergyChargedToBatteryWh += canChargeToBattery;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (netAfterPv > 0) energyFromGridThisInterval = netAfterPv;
|
|
||||||
}
|
|
||||||
|
|
||||||
energyFromGridThisInterval = Math.max(0, energyFromGridThisInterval);
|
|
||||||
totalGridEnergyWh += energyFromGridThisInterval;
|
|
||||||
|
|
||||||
timeLabels.push((currentIntervalStartSimTime).toFixed(1) + 'h');
|
|
||||||
deviceConsumptionData.push(intervalDevicePowerW.toFixed(1));
|
|
||||||
if(useBattery) batteryChargeData.push((currentBatteryEnergyWh / batteryMaxEnergyWh * 100).toFixed(1));
|
|
||||||
gridConsumptionData.push((energyFromGridThisInterval / timePerIntervalHours).toFixed(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const finalTotalCost = (totalGridEnergyWh / 1000) * costPerKwh;
|
|
||||||
|
|
||||||
energyConsumedDeviceOutput.textContent = formatEnergy(totalDeviceEnergyWh);
|
|
||||||
energyFromPvOutput.textContent = usePv ? formatEnergy(finalTotalPvEnergyWh) : '--';
|
|
||||||
energyFromBatteryOutput.textContent = useBattery ? formatEnergy(totalEnergyDrawnFromBatteryWh) : '--';
|
|
||||||
energyToBatteryOutput.textContent = useBattery ? formatEnergy(totalEnergyChargedToBatteryWh) : '--';
|
|
||||||
energyFromGridOutput.textContent = formatEnergy(totalGridEnergyWh);
|
|
||||||
totalCostOutput.textContent = finalTotalCost.toLocaleString(currentLang === 'it' ? 'it-IT' : 'en-US', { style: 'currency', currency: 'EUR' });
|
|
||||||
|
|
||||||
if (consumptionChart) consumptionChart.destroy();
|
|
||||||
const datasets = [
|
|
||||||
{ label: t.chartLabelDeviceConsumption, data: deviceConsumptionData, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.1)', tension: 0.3, fill: true, yAxisID: 'yPower' },
|
|
||||||
{ label: t.chartLabelGridDraw, data: gridConsumptionData, borderColor: 'rgba(255, 159, 64, 1)', backgroundColor: 'rgba(255, 159, 64, 0.1)', tension: 0.3, fill: true, yAxisID: 'yPower' }
|
|
||||||
];
|
|
||||||
if (usePv) {
|
|
||||||
datasets.push({ label: t.chartLabelPvProduction, data: pvProductionData_scaled.map(p => p.toFixed(1)), borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.1)', tension: 0.3, fill: true, yAxisID: 'yPower' });
|
|
||||||
datasets.push({ label: t.chartLabelAvgPvProduction, data: Array(numberOfIntervals).fill((avgDailyPvEnergyWh_target_adjusted/24).toFixed(1)), borderColor: 'rgba(75, 192, 192, 0.5)', borderDash: [3, 3], tension: 0.1, fill: false, pointRadius: 0, yAxisID: 'yPower' });
|
|
||||||
}
|
|
||||||
if (useBattery) {
|
|
||||||
datasets.push({ label: t.chartLabelBatteryCharge, data: batteryChargeData, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.3, fill: false, yAxisID: 'yBattery' });
|
|
||||||
}
|
|
||||||
|
|
||||||
consumptionChart = new Chart(consumptionChartCanvas, {
|
|
||||||
type: 'line', data: { labels: timeLabels, datasets: datasets },
|
|
||||||
options: {
|
|
||||||
responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, stacked: false,
|
|
||||||
scales: {
|
|
||||||
yPower: { type: 'linear', display: true, position: 'left', title: { display: true, text: t.chartAxisPower, font: {size: 12}, color: '#4a5568' }, grid: { color: '#e2e8f0' }, ticks: { color: '#718096'} },
|
|
||||||
yBattery: { type: 'linear', display: useBattery, position: 'right', min: 0, max: 100, title: { display: true, text: t.chartAxisBattery, font: {size: 12}, color: '#4a5568' }, grid: { drawOnChartArea: false }, ticks: { color: '#718096'} },
|
|
||||||
x: { title: { display: true, text: t.chartAxisDuration, font: {size: 14, weight: '500'}, color: '#4a5568' }, grid: { display: false }, ticks: { color: '#718096', maxRotation: 45, autoSkip: true, maxTicksLimit: 24 } }
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: { position: 'top', labels: { font: {size: 10}, color: '#4a5568'} },
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
let label = context.dataset.label || '';
|
|
||||||
if (label) label += ': ';
|
|
||||||
if (context.parsed.y !== null) {
|
|
||||||
const unit = context.dataset.yAxisID === 'yBattery' ? ' %' : ' W';
|
|
||||||
label += parseFloat(context.parsed.y).toLocaleString(currentLang === 'it' ? 'it-IT' : 'en-US', {minimumFractionDigits:1, maximumFractionDigits:1}) + unit;
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
animation: { duration: 500, easing: 'easeInOutQuart' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
updateUI(currentLang);
|
|
||||||
chartMessageP.classList.remove('hidden');
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
592
savings-calculator.html
Normal file
592
savings-calculator.html
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calcolatore di Risparmio Fotovoltaico (Completo)</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
.card-shadow {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.action-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.action-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 flex items-center justify-center min-h-screen p-4">
|
||||||
|
|
||||||
|
<div class="w-full max-w-3xl bg-white rounded-2xl card-shadow overflow-hidden">
|
||||||
|
<div class="p-6 md:p-8">
|
||||||
|
<h1 class="text-2xl md:text-3xl font-bold text-gray-800 text-center mb-2">Calcolatore Economico Fotovoltaico</h1>
|
||||||
|
<p class="text-gray-600 text-center mb-6 md:mb-8">Analizza costi, risparmi e guadagni del tuo impianto solare.</p>
|
||||||
|
|
||||||
|
<!-- Sezione Input Principali -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<label for="consumoTotale" class="block text-sm font-medium text-gray-700 mb-1">Consumo Totale Casa (kWh)</label>
|
||||||
|
<input type="number" id="consumoTotale" placeholder="Es. 450" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="consumoRete" class="block text-sm font-medium text-gray-700 mb-1">Consumo dalla Rete (kWh)</label>
|
||||||
|
<input type="number" id="consumoRete" placeholder="Es. 150" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="energiaImmessa" class="block text-sm font-medium text-gray-700 mb-1">Energia Immessa in Rete (kWh)</label>
|
||||||
|
<input type="number" id="energiaImmessa" placeholder="Es. 100" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="costoKwh" class="block text-sm font-medium text-gray-700 mb-1">Costo Acquisto kWh (€)</label>
|
||||||
|
<input type="number" id="costoKwh" step="0.01" placeholder="Es. 0.25" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="prezzoVendita" class="block text-sm font-medium text-gray-700 mb-1">Prezzo Vendita kWh (€)</label>
|
||||||
|
<input type="number" id="prezzoVendita" step="0.01" placeholder="Es. 0.10" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sezione Gestione Gruppi -->
|
||||||
|
<div class="border-t pt-6 mb-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-4 text-center">Gestione Gruppi di Utenze</h3>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-end">
|
||||||
|
<div>
|
||||||
|
<label for="groupNameInput" class="block text-sm font-medium text-gray-700 mb-1">Nome Nuovo Gruppo</label>
|
||||||
|
<input type="text" id="groupNameInput" placeholder="Es. Utenze Notturne" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
|
||||||
|
</div>
|
||||||
|
<button id="saveGroupBtn" class="w-full bg-teal-600 text-white font-semibold py-2 px-4 rounded-lg action-btn transition-colors">Salva Lista Corrente come Gruppo</button>
|
||||||
|
</div>
|
||||||
|
<div class="border-t my-4"></div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="groupSelect" class="block text-sm font-medium text-gray-700 mb-1">Gruppi Salvati</label>
|
||||||
|
<select id="groupSelect" class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white"></select>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="loadGroupBtn" class="w-full bg-blue-500 text-white font-semibold py-2 px-4 rounded-lg action-btn transition-colors">Carica</button>
|
||||||
|
<button id="deleteGroupBtn" class="w-full bg-red-500 text-white font-semibold py-2 px-4 rounded-lg action-btn transition-colors">Elimina</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sezione Utenze Specifiche -->
|
||||||
|
<div class="border-t pt-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-4 text-center">Aggiungi Utenze Specifiche (Opzionale)</h3>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4 items-end mb-4">
|
||||||
|
<div class="flex-grow w-full">
|
||||||
|
<label for="nomeUtenza" class="block text-sm font-medium text-gray-700 mb-1">Nome Utenza</label>
|
||||||
|
<input type="text" id="nomeUtenza" placeholder="Es. Auto Elettrica" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow w-full md:w-auto">
|
||||||
|
<label for="consumoUtenza" class="block text-sm font-medium text-gray-700 mb-1">Consumo (kWh)</label>
|
||||||
|
<input type="number" id="consumoUtenza" placeholder="100" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
<button id="addUtenzaBtn" class="w-full md:w-auto bg-gray-700 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-4 focus:ring-gray-300 transition-all">Aggiungi</button>
|
||||||
|
</div>
|
||||||
|
<div id="listaUtenze" class="space-y-2">
|
||||||
|
<!-- Le utenze aggiunte verranno mostrate qui -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pulsante di Calcolo -->
|
||||||
|
<div class="text-center mt-8">
|
||||||
|
<button id="calcolaBtn" class="bg-blue-600 text-white font-bold py-3 px-8 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 transition-all duration-300 transform hover:scale-105">
|
||||||
|
Calcola Bilancio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Messaggio di Errore -->
|
||||||
|
<div id="error-message" class="hidden mt-4 text-center text-red-600 font-medium p-3 bg-red-100 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sezione Risultati -->
|
||||||
|
<div id="risultati" class="hidden bg-gray-50 p-6 md:p-8 border-t border-gray-200">
|
||||||
|
|
||||||
|
<!-- Riepilogo Economico -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl md:text-2xl font-bold text-gray-800 text-center mb-6">Riepilogo Economico</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-center">
|
||||||
|
<div class="bg-white p-4 rounded-lg border border-red-200">
|
||||||
|
<p class="text-sm text-gray-500">Costo dalla Rete</p>
|
||||||
|
<p id="costoRete" class="text-2xl font-bold text-red-600">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-lg border border-green-200">
|
||||||
|
<p class="text-sm text-gray-500">Risparmio Autoconsumo</p>
|
||||||
|
<p id="risparmioFV" class="text-2xl font-bold text-green-600">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-lg border border-yellow-400">
|
||||||
|
<p class="text-sm text-gray-500">Guadagno Immissione</p>
|
||||||
|
<p id="guadagnoImmissione" class="text-2xl font-bold text-yellow-600">-</p>
|
||||||
|
</div>
|
||||||
|
<div id="bilancioCard" class="bg-white p-4 rounded-lg border-2">
|
||||||
|
<p class="text-sm font-semibold">Bilancio Finale</p>
|
||||||
|
<p id="bilancioFinale" class="text-2xl font-bold">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Riepilogo Energetico -->
|
||||||
|
<div class="mb-8 pt-6 border-t">
|
||||||
|
<h2 class="text-xl md:text-2xl font-bold text-gray-800 text-center mb-6">Riepilogo Energetico (kWh)</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-center">
|
||||||
|
<div class="bg-white p-4 rounded-lg border">
|
||||||
|
<p class="text-sm text-gray-500">Energia Autoconsumata</p>
|
||||||
|
<p id="energiaFV" class="text-2xl font-bold text-blue-600">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-lg border">
|
||||||
|
<p class="text-sm text-gray-500">Energia da Rete</p>
|
||||||
|
<p id="energiaRete" class="text-2xl font-bold text-red-600">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-lg border">
|
||||||
|
<p class="text-sm text-gray-500">Energia Immessa</p>
|
||||||
|
<p id="energiaImmessaOut" class="text-2xl font-bold text-yellow-600">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-lg border">
|
||||||
|
<p class="text-sm text-gray-500">Totale Prodotto da FV</p>
|
||||||
|
<p id="energiaProdotta" class="text-2xl font-bold text-green-600">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Analisi Costi Utenze Specifiche -->
|
||||||
|
<div id="analisi-costi-specifica" class="hidden mb-8 pt-6 border-t">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-4 text-center">Analisi Costi Utenze Specifiche</h3>
|
||||||
|
<div id="lista-costi-utenze" class="space-y-3">
|
||||||
|
<!-- I costi delle utenze verranno mostrati qui -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grafico Ripartizione Fonti -->
|
||||||
|
<div class="pt-6 border-t">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-3 text-center">Ripartizione Fonti Energetiche (Consumo Casa)</h3>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-8 overflow-hidden flex">
|
||||||
|
<div id="barraRete" class="bg-red-500 h-full flex items-center justify-center text-white text-sm font-medium transition-all duration-500" style="width: 0%;"></div>
|
||||||
|
<div id="barraFV" class="bg-green-500 h-full flex items-center justify-center text-white text-sm font-medium transition-all duration-500" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-2 text-sm">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="w-3 h-3 bg-red-500 rounded-full mr-2"></span>
|
||||||
|
<span id="percentualeRete">Dalla Rete: -%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="w-3 h-3 bg-green-500 rounded-full mr-2"></span>
|
||||||
|
<span id="percentualeFV">Da Fotovoltaico: -%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grafico Incidenza Consumi -->
|
||||||
|
<div id="grafico-specifica" class="hidden mt-8 pt-6 border-t">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-3 text-center">Incidenza Utenze sui Consumi Totali</h3>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-8 overflow-hidden flex">
|
||||||
|
<div id="barraSpecifica" class="bg-purple-600 h-full flex items-center justify-center text-white text-sm font-medium transition-all duration-500" style="width: 0%;"></div>
|
||||||
|
<div id="barraAltri" class="bg-gray-400 h-full flex items-center justify-center text-white text-sm font-medium transition-all duration-500" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-2 text-sm">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="w-3 h-3 bg-purple-600 rounded-full mr-2"></span>
|
||||||
|
<span id="percentualeSpecifica">Totale Utenze Specifiche: -%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="w-3 h-3 bg-gray-400 rounded-full mr-2"></span>
|
||||||
|
<span id="percentualeAltri">Altri Consumi: -%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// --- STATE MANAGEMENT ---
|
||||||
|
let specificUtilities = [];
|
||||||
|
let utilityGroups = [];
|
||||||
|
|
||||||
|
// --- DOM REFERENCES ---
|
||||||
|
// Inputs
|
||||||
|
const consumoTotaleInput = document.getElementById('consumoTotale');
|
||||||
|
const consumoReteInput = document.getElementById('consumoRete');
|
||||||
|
const energiaImmessaInput = document.getElementById('energiaImmessa');
|
||||||
|
const costoKwhInput = document.getElementById('costoKwh');
|
||||||
|
const prezzoVenditaInput = document.getElementById('prezzoVendita');
|
||||||
|
// Utenze
|
||||||
|
const nomeUtenzaInput = document.getElementById('nomeUtenza');
|
||||||
|
const consumoUtenzaInput = document.getElementById('consumoUtenza');
|
||||||
|
const addUtenzaBtn = document.getElementById('addUtenzaBtn');
|
||||||
|
const listaUtenzeDiv = document.getElementById('listaUtenze');
|
||||||
|
// Gruppi
|
||||||
|
const groupNameInput = document.getElementById('groupNameInput');
|
||||||
|
const saveGroupBtn = document.getElementById('saveGroupBtn');
|
||||||
|
const groupSelect = document.getElementById('groupSelect');
|
||||||
|
const loadGroupBtn = document.getElementById('loadGroupBtn');
|
||||||
|
const deleteGroupBtn = document.getElementById('deleteGroupBtn');
|
||||||
|
// Buttons & Messages
|
||||||
|
const calcolaBtn = document.getElementById('calcolaBtn');
|
||||||
|
const risultatiDiv = document.getElementById('risultati');
|
||||||
|
const errorMessageDiv = document.getElementById('error-message');
|
||||||
|
// Results Sections
|
||||||
|
const costoReteEl = document.getElementById('costoRete');
|
||||||
|
const risparmioFVEl = document.getElementById('risparmioFV');
|
||||||
|
const guadagnoImmissioneEl = document.getElementById('guadagnoImmissione');
|
||||||
|
const bilancioFinaleEl = document.getElementById('bilancioFinale');
|
||||||
|
const bilancioCardEl = document.getElementById('bilancioCard');
|
||||||
|
const energiaFVEl = document.getElementById('energiaFV');
|
||||||
|
const energiaReteEl = document.getElementById('energiaRete');
|
||||||
|
const energiaImmessaOutEl = document.getElementById('energiaImmessaOut');
|
||||||
|
const energiaProdottaEl = document.getElementById('energiaProdotta');
|
||||||
|
const analisiCostiSpecificaDiv = document.getElementById('analisi-costi-specifica');
|
||||||
|
const listaCostiUtenzeDiv = document.getElementById('lista-costi-utenze');
|
||||||
|
// Graphs
|
||||||
|
const barraReteEl = document.getElementById('barraRete');
|
||||||
|
const barraFVEl = document.getElementById('barraFV');
|
||||||
|
const percentualeReteEl = document.getElementById('percentualeRete');
|
||||||
|
const percentualeFVEl = document.getElementById('percentualeFV');
|
||||||
|
const graficoSpecificaDiv = document.getElementById('grafico-specifica');
|
||||||
|
const barraSpecificaEl = document.getElementById('barraSpecifica');
|
||||||
|
const barraAltriEl = document.getElementById('barraAltri');
|
||||||
|
const percentualeSpecificaEl = document.getElementById('percentualeSpecifica');
|
||||||
|
const percentualeAltriEl = document.getElementById('percentualeAltri');
|
||||||
|
|
||||||
|
// --- FUNCTIONS ---
|
||||||
|
const renderUtilitiesList = () => {
|
||||||
|
listaUtenzeDiv.innerHTML = '';
|
||||||
|
if (specificUtilities.length === 0) {
|
||||||
|
listaUtenzeDiv.innerHTML = `<p class="text-center text-gray-500 text-sm">Nessuna utenza specifica aggiunta.</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
specificUtilities.forEach((utenza, index) => {
|
||||||
|
const utenzaEl = document.createElement('div');
|
||||||
|
utenzaEl.className = 'bg-gray-100 p-3 rounded-lg';
|
||||||
|
|
||||||
|
if (utenza.isEditing) {
|
||||||
|
utenzaEl.innerHTML = `
|
||||||
|
<div class="flex flex-col sm:flex-row gap-2">
|
||||||
|
<input type="text" value="${utenza.name}" data-type="name" data-index="${index}" class="edit-input-name w-full px-2 py-1 border border-gray-300 rounded-md" placeholder="Nome Utenza">
|
||||||
|
<input type="number" value="${utenza.consumption}" data-type="consumption" data-index="${index}" class="edit-input-consumption w-full sm:w-32 px-2 py-1 border border-gray-300 rounded-md" placeholder="kWh">
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<button data-index="${index}" class="save-btn action-btn bg-green-500 text-white text-xs font-bold py-1 px-3 rounded-md transition-colors">Salva</button>
|
||||||
|
<button data-index="${index}" class="cancel-btn action-btn bg-gray-400 text-white text-xs font-bold py-1 px-3 rounded-md transition-colors">Annulla</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
utenzaEl.innerHTML = `
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-gray-800">${utenza.name}</span>:
|
||||||
|
<span class="text-gray-600">${utenza.consumption} kWh</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button data-index="${index}" class="edit-btn action-btn bg-blue-500 text-white text-xs font-bold py-1 px-3 rounded-md transition-colors">Modifica</button>
|
||||||
|
<button data-index="${index}" class="remove-btn action-btn bg-red-500 text-white text-xs font-bold py-1 px-3 rounded-md transition-colors">Rimuovi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
listaUtenzeDiv.appendChild(utenzaEl);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUtenza = () => {
|
||||||
|
const name = nomeUtenzaInput.value.trim();
|
||||||
|
const consumption = parseFloat(consumoUtenzaInput.value);
|
||||||
|
|
||||||
|
if (!name || isNaN(consumption) || consumption <= 0) {
|
||||||
|
alert('Per favore, inserisci un nome valido e un consumo positivo per l\'utenza.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
specificUtilities.push({ name, consumption, isEditing: false });
|
||||||
|
nomeUtenzaInput.value = '';
|
||||||
|
consumoUtenzaInput.value = '';
|
||||||
|
nomeUtenzaInput.focus();
|
||||||
|
renderUtilitiesList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const mostraErrore = (messaggio) => {
|
||||||
|
errorMessageDiv.textContent = messaggio;
|
||||||
|
errorMessageDiv.classList.remove('hidden');
|
||||||
|
risultatiDiv.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
const nascondiErrore = () => {
|
||||||
|
errorMessageDiv.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- STORAGE FUNCTIONS ---
|
||||||
|
const savePricesToStorage = () => {
|
||||||
|
localStorage.setItem('photovoltaicCalculator_costoKwh', costoKwhInput.value);
|
||||||
|
localStorage.setItem('photovoltaicCalculator_prezzoVendita', prezzoVenditaInput.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPricesFromStorage = () => {
|
||||||
|
const storedCosto = localStorage.getItem('photovoltaicCalculator_costoKwh');
|
||||||
|
const storedPrezzo = localStorage.getItem('photovoltaicCalculator_prezzoVendita');
|
||||||
|
if (storedCosto) {
|
||||||
|
costoKwhInput.value = storedCosto;
|
||||||
|
}
|
||||||
|
if (storedPrezzo) {
|
||||||
|
prezzoVenditaInput.value = storedPrezzo;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveGroupsToStorage = () => {
|
||||||
|
localStorage.setItem('photovoltaicCalculatorGroups', JSON.stringify(utilityGroups));
|
||||||
|
};
|
||||||
|
|
||||||
|
const populateGroupSelect = () => {
|
||||||
|
groupSelect.innerHTML = '<option value="">-- Seleziona un gruppo --</option>';
|
||||||
|
utilityGroups.forEach(group => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = group.groupName;
|
||||||
|
option.textContent = group.groupName;
|
||||||
|
groupSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadGroupsFromStorage = () => {
|
||||||
|
try {
|
||||||
|
const storedGroups = localStorage.getItem('photovoltaicCalculatorGroups');
|
||||||
|
if (storedGroups) {
|
||||||
|
utilityGroups = JSON.parse(storedGroups);
|
||||||
|
populateGroupSelect();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Errore nel caricamento dei gruppi dal localStorage:", error);
|
||||||
|
utilityGroups = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- EVENT LISTENERS ---
|
||||||
|
addUtenzaBtn.addEventListener('click', addUtenza);
|
||||||
|
costoKwhInput.addEventListener('input', savePricesToStorage);
|
||||||
|
prezzoVenditaInput.addEventListener('input', savePricesToStorage);
|
||||||
|
|
||||||
|
|
||||||
|
listaUtenzeDiv.addEventListener('click', (e) => {
|
||||||
|
const button = e.target.closest('button');
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const index = parseInt(button.dataset.index, 10);
|
||||||
|
|
||||||
|
if (button.classList.contains('remove-btn')) {
|
||||||
|
specificUtilities.splice(index, 1);
|
||||||
|
} else if (button.classList.contains('edit-btn')) {
|
||||||
|
specificUtilities.forEach((u, i) => u.isEditing = (i === index));
|
||||||
|
} else if (button.classList.contains('cancel-btn')) {
|
||||||
|
specificUtilities[index].isEditing = false;
|
||||||
|
} else if (button.classList.contains('save-btn')) {
|
||||||
|
const parentDiv = button.closest('.flex');
|
||||||
|
const newName = parentDiv.querySelector('.edit-input-name').value.trim();
|
||||||
|
const newConsumption = parseFloat(parentDiv.querySelector('.edit-input-consumption').value);
|
||||||
|
|
||||||
|
if (newName && !isNaN(newConsumption) && newConsumption > 0) {
|
||||||
|
specificUtilities[index].name = newName;
|
||||||
|
specificUtilities[index].consumption = newConsumption;
|
||||||
|
specificUtilities[index].isEditing = false;
|
||||||
|
} else {
|
||||||
|
alert('Per favore, inserisci dati validi prima di salvare.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderUtilitiesList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group Event Listeners
|
||||||
|
saveGroupBtn.addEventListener('click', () => {
|
||||||
|
const groupName = groupNameInput.value.trim();
|
||||||
|
if (!groupName) {
|
||||||
|
alert('Per favore, inserisci un nome per il gruppo.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (specificUtilities.length === 0) {
|
||||||
|
alert('Aggiungi almeno un\'utenza alla lista prima di salvare un gruppo.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingGroupIndex = utilityGroups.findIndex(g => g.groupName === groupName);
|
||||||
|
if (existingGroupIndex !== -1) {
|
||||||
|
if (!confirm(`Un gruppo con nome "${groupName}" esiste già. Vuoi sovrascriverlo?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanUtilities = specificUtilities.map(({ name, consumption }) => ({ name, consumption }));
|
||||||
|
const newGroup = { groupName, utilities: cleanUtilities };
|
||||||
|
|
||||||
|
if (existingGroupIndex !== -1) {
|
||||||
|
utilityGroups[existingGroupIndex] = newGroup;
|
||||||
|
} else {
|
||||||
|
utilityGroups.push(newGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveGroupsToStorage();
|
||||||
|
populateGroupSelect();
|
||||||
|
groupSelect.value = groupName;
|
||||||
|
groupNameInput.value = '';
|
||||||
|
alert(`Gruppo "${groupName}" salvato con successo!`);
|
||||||
|
});
|
||||||
|
|
||||||
|
loadGroupBtn.addEventListener('click', () => {
|
||||||
|
const selectedGroupName = groupSelect.value;
|
||||||
|
if (!selectedGroupName) {
|
||||||
|
alert('Per favore, seleziona un gruppo da caricare.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupToLoad = utilityGroups.find(g => g.groupName === selectedGroupName);
|
||||||
|
if (groupToLoad) {
|
||||||
|
specificUtilities = groupToLoad.utilities.map(u => ({ ...u, isEditing: false }));
|
||||||
|
renderUtilitiesList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteGroupBtn.addEventListener('click', () => {
|
||||||
|
const selectedGroupName = groupSelect.value;
|
||||||
|
if (!selectedGroupName) {
|
||||||
|
alert('Per favore, seleziona un gruppo da eliminare.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirm(`Sei sicuro di voler eliminare il gruppo "${selectedGroupName}"?`)) {
|
||||||
|
utilityGroups = utilityGroups.filter(g => g.groupName !== selectedGroupName);
|
||||||
|
saveGroupsToStorage();
|
||||||
|
populateGroupSelect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
calcolaBtn.addEventListener('click', () => {
|
||||||
|
// 1. Read and Validate Inputs
|
||||||
|
const consumoTotale = parseFloat(consumoTotaleInput.value);
|
||||||
|
const consumoRete = parseFloat(consumoReteInput.value);
|
||||||
|
const energiaImmessa = parseFloat(energiaImmessaInput.value);
|
||||||
|
const costoKwh = parseFloat(costoKwhInput.value);
|
||||||
|
const prezzoVendita = parseFloat(prezzoVenditaInput.value);
|
||||||
|
|
||||||
|
if ([consumoTotale, consumoRete, energiaImmessa, costoKwh, prezzoVendita].some(isNaN) || consumoTotale <= 0 || costoKwh < 0 || prezzoVendita < 0) {
|
||||||
|
mostraErrore("Inserire valori validi. I consumi totali devono essere > 0. I prezzi non possono essere negativi.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (consumoRete < 0 || energiaImmessa < 0) {
|
||||||
|
mostraErrore("I valori di consumo/immissione non possono essere negativi.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (consumoRete > consumoTotale) {
|
||||||
|
mostraErrore("Il consumo dalla rete non può superare il consumo totale della casa.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const totalSpecificConsumption = specificUtilities.reduce((sum, u) => sum + u.consumption, 0);
|
||||||
|
if (totalSpecificConsumption > consumoTotale) {
|
||||||
|
mostraErrore("La somma dei consumi delle utenze specifiche non può superare il consumo totale della casa.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nascondiErrore();
|
||||||
|
|
||||||
|
// 2. Core Calculations
|
||||||
|
const energiaAutoconsumata = consumoTotale - consumoRete;
|
||||||
|
const energiaProdottaTotale = energiaAutoconsumata + energiaImmessa;
|
||||||
|
const costoDaRete = consumoRete * costoKwh;
|
||||||
|
const risparmioDaFV = energiaAutoconsumata * costoKwh;
|
||||||
|
const guadagnoDaImmissione = energiaImmessa * prezzoVendita;
|
||||||
|
const bilancioFinale = risparmioDaFV + guadagnoDaImmissione - costoDaRete;
|
||||||
|
|
||||||
|
// 3. Update UI - Economic & Energy Summary
|
||||||
|
costoReteEl.textContent = `${costoDaRete.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' })}`;
|
||||||
|
risparmioFVEl.textContent = `${risparmioDaFV.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' })}`;
|
||||||
|
guadagnoImmissioneEl.textContent = `${guadagnoDaImmissione.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' })}`;
|
||||||
|
bilancioFinaleEl.textContent = `${bilancioFinale.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' })}`;
|
||||||
|
|
||||||
|
bilancioCardEl.classList.toggle('border-green-400', bilancioFinale >= 0);
|
||||||
|
bilancioCardEl.classList.toggle('border-red-400', bilancioFinale < 0);
|
||||||
|
bilancioFinaleEl.classList.toggle('text-green-600', bilancioFinale >= 0);
|
||||||
|
bilancioFinaleEl.classList.toggle('text-red-600', bilancioFinale < 0);
|
||||||
|
|
||||||
|
energiaFVEl.textContent = `${energiaAutoconsumata.toFixed(2)} kWh`;
|
||||||
|
energiaReteEl.textContent = `${consumoRete.toFixed(2)} kWh`;
|
||||||
|
energiaImmessaOutEl.textContent = `${energiaImmessa.toFixed(2)} kWh`;
|
||||||
|
energiaProdottaEl.textContent = `${energiaProdottaTotale.toFixed(2)} kWh`;
|
||||||
|
|
||||||
|
// 4. Update UI - Specific Utilities Cost Analysis
|
||||||
|
if (specificUtilities.length > 0) {
|
||||||
|
const costoMedioKwhConsumato = consumoTotale > 0 ? costoDaRete / consumoTotale : 0;
|
||||||
|
listaCostiUtenzeDiv.innerHTML = '';
|
||||||
|
let costoTotaleUtenzeSpecifiche = 0;
|
||||||
|
|
||||||
|
specificUtilities.forEach(utenza => {
|
||||||
|
const costoUtenza = utenza.consumption * costoMedioKwhConsumato;
|
||||||
|
costoTotaleUtenzeSpecifiche += costoUtenza;
|
||||||
|
const costoEl = document.createElement('div');
|
||||||
|
costoEl.className = 'flex justify-between items-center bg-white p-3 rounded-lg border';
|
||||||
|
costoEl.innerHTML = `
|
||||||
|
<span class="font-medium text-gray-800">${utenza.name}</span>
|
||||||
|
<span class="font-bold text-purple-700">${costoUtenza.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' })}</span>
|
||||||
|
`;
|
||||||
|
listaCostiUtenzeDiv.appendChild(costoEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggiungi la riga del totale
|
||||||
|
const totaleCostoEl = document.createElement('div');
|
||||||
|
totaleCostoEl.className = 'flex justify-between items-center bg-gray-200 p-3 rounded-lg border-t-2 border-gray-300 mt-3';
|
||||||
|
totaleCostoEl.innerHTML = `
|
||||||
|
<span class="font-bold text-gray-800">Costo Totale Utenze</span>
|
||||||
|
<span class="font-extrabold text-purple-800">${costoTotaleUtenzeSpecifiche.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' })}</span>
|
||||||
|
`;
|
||||||
|
listaCostiUtenzeDiv.appendChild(totaleCostoEl);
|
||||||
|
|
||||||
|
analisiCostiSpecificaDiv.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
analisiCostiSpecificaDiv.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Update UI - Graphs
|
||||||
|
const percRete = consumoTotale > 0 ? (consumoRete / consumoTotale) * 100 : 0;
|
||||||
|
const percFV = consumoTotale > 0 ? (energiaAutoconsumata / consumoTotale) * 100 : 0;
|
||||||
|
barraReteEl.style.width = `${percRete}%`;
|
||||||
|
barraFVEl.style.width = `${percFV}%`;
|
||||||
|
barraReteEl.textContent = percRete > 10 ? `${percRete.toFixed(1)}%` : '';
|
||||||
|
barraFVEl.textContent = percFV > 10 ? `${percFV.toFixed(1)}%` : '';
|
||||||
|
percentualeReteEl.textContent = `Dalla Rete: ${percRete.toFixed(1)}%`;
|
||||||
|
percentualeFVEl.textContent = `Da Fotovoltaico: ${percFV.toFixed(1)}%`;
|
||||||
|
|
||||||
|
if (totalSpecificConsumption > 0) {
|
||||||
|
graficoSpecificaDiv.classList.remove('hidden');
|
||||||
|
const altriConsumi = consumoTotale - totalSpecificConsumption;
|
||||||
|
const percSpecifica = (totalSpecificConsumption / consumoTotale) * 100;
|
||||||
|
const percAltri = (altriConsumi / consumoTotale) * 100;
|
||||||
|
|
||||||
|
barraSpecificaEl.style.width = `${percSpecifica}%`;
|
||||||
|
barraAltriEl.style.width = `${percAltri}%`;
|
||||||
|
barraSpecificaEl.textContent = percSpecifica > 10 ? `${percSpecifica.toFixed(1)}%` : '';
|
||||||
|
barraAltriEl.textContent = percAltri > 10 ? `${percAltri.toFixed(1)}%` : '';
|
||||||
|
percentualeSpecificaEl.textContent = `Utenze Specifiche: ${percSpecifica.toFixed(1)}% (${totalSpecificConsumption.toFixed(1)} kWh)`;
|
||||||
|
percentualeAltriEl.textContent = `Altri Consumi: ${percAltri.toFixed(1)}% (${altriConsumi.toFixed(1)} kWh)`;
|
||||||
|
} else {
|
||||||
|
graficoSpecificaDiv.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Show Results
|
||||||
|
risultatiDiv.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- INITIAL RENDER ---
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadGroupsFromStorage();
|
||||||
|
loadPricesFromStorage();
|
||||||
|
renderUtilitiesList();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user