\n"); exit(1); } [$script, $xmlFile, $deltaPriceMWh, $maxCapWh, $startSocWh, $chargeEffPct, $dischargeEffPct, $chargePowerW, $dischargePowerW, $buyinCost, $sellCost, $energyTax, $vatRatePct] = $argv; $deltaPriceMWh = floatval($deltaPriceMWh); $maxCapWh = floatval($maxCapWh); $startSocWh = floatval($startSocWh); $chargeEffPct = floatval($chargeEffPct); $dischargeEffPct = floatval($dischargeEffPct); $chargePowerW = floatval($chargePowerW); $dischargePowerW = floatval($dischargePowerW); $buyinCost = floatval($buyinCost); $sellCost = floatval($sellCost); $energyTax = floatval($energyTax); $vatRatePct = floatval($vatRatePct); if (!is_file($xmlFile)) { fwrite(STDERR, "Error: XML file not found: $xmlFile\n"); exit(1); } // Extract target date from filename if (!preg_match('/(\\d{4})(\\d{2})(\\d{2})/', basename($xmlFile), $m)) { fwrite(STDERR, "Error: Cannot extract date from filename.\n"); exit(1); } $targetDate = sprintf('%04d-%02d-%02d', $m[1], $m[2], $m[3]); function parseEntsoeXml(string $file, string $targetDate): array { $doc = new DOMDocument(); $doc->load($file); $xpath = new DOMXPath($doc); $xpath->registerNamespace('ns', 'urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:3'); $slots = []; $periods = $xpath->query('//ns:TimeSeries/ns:Period'); $tzLocal = new DateTimeZone('Europe/Amsterdam'); foreach ($periods as $period) { $startNode = $xpath->query('ns:timeInterval/ns:start', $period)->item(0); if (!$startNode) continue; $start = DateTimeImmutable::createFromFormat('Y-m-d\TH:i\Z', $startNode->textContent, new DateTimeZone('UTC')); $dt15m = new DateInterval('PT15M'); $points = []; foreach ($xpath->query('ns:Point', $period) as $pt) { $posNode = $xpath->query('ns:position', $pt)->item(0); $priceNode = $xpath->query('ns:price.amount', $pt)->item(0); if (!$posNode || !$priceNode) continue; $points[] = ['pos' => intval($posNode->textContent), 'price_mwh' => floatval($priceNode->textContent)]; } usort($points, fn($a, $b) => $a['pos'] <=> $b['pos']); $curr = $start; foreach ($points as $p) { $localTs = $curr->setTimezone($tzLocal); if ($localTs->format('Y-m-d') === $targetDate) { $slots[] = ['ts' => $localTs, 'price_mwh' => $p['price_mwh']]; } $curr = $curr->add($dt15m); } } usort($slots, fn($a, $b) => $a['ts'] <=> $b['ts']); return $slots; } $slots = parseEntsoeXml($xmlFile, $targetDate); if (empty($slots)) { fwrite(STDERR, "No data found for $targetDate.\n"); exit(0); } // Averages & thresholds $avgPriceMWh = array_sum(array_column($slots, 'price_mwh')) / max(count($slots), 1); $buyTh = $avgPriceMWh - ($deltaPriceMWh / 2.0); $sellTh = $avgPriceMWh + ($deltaPriceMWh / 2.0); // Sorted cheapest & most expensive arrays $sortedAsc = $slots; $sortedDesc = $slots; usort($sortedAsc, fn($a, $b) => $a['price_mwh'] <=> $b['price_mwh']); usort($sortedDesc, fn($a, $b) => $b['price_mwh'] <=> $a['price_mwh']); // Simulation state $socWh = $startSocWh; $chargeEff = $chargeEffPct / 100.0; $dischargeEff = $dischargeEffPct / 100.0; $vatMult = 1.0 + ($vatRatePct / 100.0); $slotWhChargeMax = $chargePowerW * 0.25; // 15 min $slotWhDischargeMaxGrid = $dischargePowerW * 0.25; $totalInWh = 0.0; $totalOutWh = 0.0; $totalLossWh = 0.0; $profitAcc = 0.0; $logRows = []; foreach ($slots as $s) { $ts = $s['ts']; $priceMWh = $s['price_mwh']; $priceKWh = $priceMWh / 1000.0; $action = 'Idle'; $cWhIn = 0.0; $dWhOut = 0.0; $slotProfit = 0.0; if ($priceMWh <= $buyTh && $socWh < $maxCapWh) { $spaceWh = $maxCapWh - $socWh; $gridNeededWh = $spaceWh / $chargeEff; $gridInWh = min($slotWhChargeMax, $gridNeededWh); $storedWh = $gridInWh * $chargeEff; $socWh += $storedWh; $totalInWh += $gridInWh; $lossWh = $gridInWh - $storedWh; $totalLossWh += $lossWh; $costPerKWh = $priceKWh + $buyinCost + $energyTax; $slotProfit -= ($gridInWh / 1000.0) * $costPerKWh * $vatMult; $action = 'Charge'; $cWhIn = $gridInWh; } elseif ($priceMWh >= $sellTh && $socWh > 0.0) { $maxGridOutByPower = $slotWhDischargeMaxGrid; $maxGridOutBySoc = $socWh * $dischargeEff; $gridOutWh = min($maxGridOutByPower, $maxGridOutBySoc); $battUsedWh = $gridOutWh / $dischargeEff; $socWh -= $battUsedWh; $totalOutWh += $gridOutWh; $lossWh = $battUsedWh - $gridOutWh; $totalLossWh += $lossWh; $revenuePerKWh = max($priceKWh - $sellCost, 0.0); $slotProfit += ($gridOutWh / 1000.0) * $revenuePerKWh * $vatMult; $action = 'Discharge'; $dWhOut = $gridOutWh; } $profitAcc += $slotProfit; $logRows[] = [ 'ts' => $ts->format('Y-m-d H:i'), // Local time Europe/Amsterdam 'price_mwh' => $priceMWh, 'action' => $action, 'soc_wh' => $socWh, 'profit_eur' => $profitAcc, 'c_energy_wh' => $cWhIn, 'd_energy_wh' => $dWhOut ]; } // -------- Presentation -------- echo "\nšŸ“Š Simulation Results for $targetDate (Europe/Amsterdam)\n" . str_repeat('-', 50) . "\n"; printf("Total Energy Charged (Grid Input) : %.2f Wh\n", $totalInWh); printf("Total Energy Discharged (Grid Output): %.2f Wh\n", $totalOutWh); printf("Total Energy Lost (C/D losses) : %.2f Wh\n", $totalLossWh); printf("Total Value Generated (Net Profit) : %.2f €\n", $profitAcc); echo str_repeat('=', 50) . "\n\n"; echo "Cheapest slots (top 10):\n"; for ($i = 0; $i < min(10, count($sortedAsc)); $i++) { $row = $sortedAsc[$i]; printf("%s | %.2f €/MWh\n", $row['ts']->format('Y-m-d H:i'), $row['price_mwh']); } echo "\nMost expensive slots (top 10):\n"; for ($i = 0; $i < min(10, count($sortedDesc)); $i++) { $row = $sortedDesc[$i]; printf("%s | %.2f €/MWh\n", $row['ts']->format('Y-m-d H:i'), $row['price_mwh']); } echo "\nDetailed 15-Minute Log:\n"; echo "Datetime | Price (€/MWh) | Action | SOC (Wh) | Profit (€) | C/D Energy (Wh)\n"; echo str_repeat('-', 95) . "\n"; foreach ($logRows as $r) { printf("%s | %10.2f | %-10s | %10.2f | %10.2f | %6.2f / %6.2f\n", $r['ts'], $r['price_mwh'], $r['action'], $r['soc_wh'], $r['profit_eur'], $r['c_energy_wh'], $r['d_energy_wh'] ); }