Last active
October 17, 2025 19:28
-
-
Save developerfromjokela/edbc943aa8ebfc0f4e9d20a70f1af754 to your computer and use it in GitHub Desktop.
Nordpool market price for OpenCARWINGS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| // Handle POST request with timezone data | |
| $postData = null; | |
| $timezoneOffset = 3; // Default EEST offset in hours | |
| $userTimezone = 'Europe/Helsinki'; // Default timezone | |
| $zone = "FI"; | |
| $vat = 1.255; | |
| if (isset($_GET['z'])) { | |
| $zone = $_GET['z']; | |
| } | |
| if (isset($_GET['vat'])) { | |
| $vat = floatval($_GET['vat']); | |
| } | |
| define("ZONE", $zone); | |
| define("VAT", $vat); | |
| if ($_SERVER['REQUEST_METHOD'] === 'POST') { | |
| $postJson = file_get_contents('php://input'); | |
| $postData = json_decode($postJson, true); | |
| if ($postData && isset($postData['tz'])) { | |
| // Use timezone offset from POST data | |
| $timezoneOffset = floatval($postData['tz']); | |
| // Set timezone based on offset (approximate) | |
| $timezoneMap = [ | |
| -12 => 'Pacific/Kwajalein', | |
| -11 => 'Pacific/Midway', | |
| -10 => 'Pacific/Honolulu', | |
| -9 => 'America/Anchorage', | |
| -8 => 'America/Los_Angeles', | |
| -7 => 'America/Denver', | |
| -6 => 'America/Chicago', | |
| -5 => 'America/New_York', | |
| -4 => 'America/Halifax', | |
| -3 => 'America/Sao_Paulo', | |
| -2 => 'Atlantic/South_Georgia', | |
| -1 => 'Atlantic/Azores', | |
| 0 => 'Europe/London', | |
| 1 => 'Europe/Berlin', | |
| 2 => 'Europe/Athens', | |
| 3 => 'Europe/Helsinki', | |
| 4 => 'Asia/Dubai', | |
| 5 => 'Asia/Karachi', | |
| 6 => 'Asia/Dhaka', | |
| 7 => 'Asia/Bangkok', | |
| 8 => 'Asia/Shanghai', | |
| 9 => 'Asia/Tokyo', | |
| 10 => 'Australia/Sydney', | |
| 11 => 'Pacific/Norfolk', | |
| 12 => 'Pacific/Auckland' | |
| ]; | |
| $userTimezone = $timezoneMap[intval($timezoneOffset)] ?? 'Europe/Helsinki'; | |
| } | |
| } | |
| // Set timezone | |
| date_default_timezone_set($userTimezone); | |
| // Approximate sunrise/sunset based on timezone offset and season | |
| function approximateSunriseSunset($timezoneOffset) { | |
| $currentTime = time(); | |
| $dayOfYear = intval(date('z', $currentTime)); // 0-365 | |
| // Base times (in hours, 24h format) - roughly for mid-latitudes | |
| $baseSunrise = 6.0; | |
| $baseSunset = 18.0; | |
| // Seasonal variation (more extreme closer to poles, less at equator) | |
| $seasonalVariation = 2.5 * sin(2 * pi() * ($dayOfYear - 80) / 365); | |
| // Latitude approximation based on timezone (very rough) | |
| $latitudeEffect = 0; | |
| if ($timezoneOffset > -6 && $timezoneOffset < 12) { | |
| // Northern hemisphere-ish | |
| $latitudeEffect = abs($timezoneOffset) * 0.1; | |
| } | |
| $sunrise = $baseSunrise - $seasonalVariation - $latitudeEffect; | |
| $sunset = $baseSunset + $seasonalVariation + $latitudeEffect; | |
| // Clamp to reasonable bounds | |
| $sunrise = max(4.0, min(8.0, $sunrise)); | |
| $sunset = max(16.0, min(20.0, $sunset)); | |
| return [$sunrise, $sunset]; | |
| } | |
| // Get approximate sunrise/sunset times | |
| list($sunriseHour, $sunsetHour) = approximateSunriseSunset($timezoneOffset); | |
| $currentHour = intval(date('H')) + (intval(date('i')) / 60.0); // Include minutes for precision | |
| // Dark mode based on approximate sunrise/sunset | |
| $isDarkMode = ($currentHour < $sunriseHour || $currentHour > $sunsetHour); | |
| function fetchDayAheadPrices($date) { | |
| $url = 'https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices?date=' . $date . '&market=DayAhead&deliveryArea='.ZONE.'¤cy=EUR'; | |
| $ch = curl_init(); | |
| curl_setopt($ch, CURLOPT_URL, $url); | |
| curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
| curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | |
| curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); | |
| $jsonString = curl_exec($ch); | |
| $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
| curl_close($ch); | |
| if ($jsonString === false || $httpCode !== 200) { | |
| return false; | |
| } | |
| $data = json_decode($jsonString, true); | |
| if (json_last_error() !== JSON_ERROR_NONE) { | |
| return false; | |
| } | |
| return $data; | |
| } | |
| function processPriceData($data) { | |
| $entries = $data['multiAreaEntries'] ?? []; | |
| $timestamps = []; | |
| $prices = []; | |
| foreach ($entries as $entry) { | |
| $start = new DateTime($entry['deliveryStart']); | |
| $end = new DateTime($entry['deliveryEnd']); | |
| $timestamp = $start->getTimestamp(); | |
| if (isset($entry['entryPerArea'][ZONE])) { | |
| $timestamps[] = [ | |
| 'start' => $timestamp, | |
| 'end' => $end->getTimestamp(), | |
| 'start_formatted' => $start->format('H:i'), | |
| 'end_formatted' => $end->format('H:i') | |
| ]; | |
| // Convert EUR/MWh to cents/kWh and apply VAT | |
| $prices[] = ($entry['entryPerArea'][ZONE] * 0.1) * VAT; | |
| } | |
| } | |
| // Sort by timestamp | |
| $dataPoints = array_combine(array_column($timestamps, 'start'), array_map(function($t, $p) { | |
| return ['timestamp' => $t, 'price' => $p]; | |
| }, $timestamps, $prices)); | |
| ksort($dataPoints); | |
| $sortedTimestamps = []; | |
| $sortedPrices = []; | |
| foreach ($dataPoints as $dataPoint) { | |
| $sortedTimestamps[] = $dataPoint['timestamp']; | |
| $sortedPrices[] = $dataPoint['price']; | |
| } | |
| return ['timestamps' => $sortedTimestamps, 'prices' => $sortedPrices]; | |
| } | |
| function findCheapestHours($timestamps, $prices, $count = 8) { | |
| // Create array of timestamp-price pairs | |
| $pairs = []; | |
| for ($i = 0; $i < count($timestamps); $i++) { | |
| $pairs[] = [ | |
| 'start' => $timestamps[$i]['start'], | |
| 'end' => $timestamps[$i]['end'], | |
| 'start_formatted' => $timestamps[$i]['start_formatted'], | |
| 'end_formatted' => $timestamps[$i]['end_formatted'], | |
| 'price' => $prices[$i] | |
| ]; | |
| } | |
| // Sort by price (ascending) | |
| usort($pairs, function($a, $b) { | |
| return $a['price'] <=> $b['price']; | |
| }); | |
| // Get cheapest periods and sort by start time | |
| $cheapestPeriods = array_slice($pairs, 0, $count); | |
| usort($cheapestPeriods, function($a, $b) { | |
| return $a['start'] <=> $b['start']; | |
| }); | |
| return $cheapestPeriods; | |
| } | |
| function mergeCheapestHoursIntoRanges($cheapestPeriods, $timestamps, $prices) { | |
| if (empty($cheapestPeriods)) { | |
| return []; | |
| } | |
| $mergedRanges = []; | |
| $i = 0; | |
| $totalPeriods = count($cheapestPeriods); | |
| // Create a map of timestamps to prices for quick lookup | |
| $priceMap = []; | |
| for ($j = 0; $j < count($timestamps); $j++) { | |
| $priceMap[$timestamps[$j]['start']] = $prices[$j]; | |
| } | |
| while ($i < $totalPeriods) { | |
| $current = $cheapestPeriods[$i]; | |
| $startTime = $current['start']; | |
| $startHour = date('H', $startTime); | |
| $startMinutes = date('i', $startTime); | |
| // Check if we can form a complete hour (4 consecutive 15-minute periods) | |
| if ($startMinutes == '00' && ($i + 3) < $totalPeriods) { | |
| $consecutive = true; | |
| $endTime = $current['end']; | |
| $endIndex = $i; | |
| $priceSum = $current['price']; | |
| $priceCount = 1; | |
| // Verify the next three periods are consecutive | |
| for ($j = 1; $j <= 3; $j++) { | |
| if ($i + $j >= $totalPeriods) { | |
| $consecutive = false; | |
| break; | |
| } | |
| $next = $cheapestPeriods[$i + $j]; | |
| if ($next['start'] != $endTime) { | |
| $consecutive = false; | |
| break; | |
| } | |
| $endTime = $next['end']; | |
| $endIndex = $i + $j; | |
| $priceSum += $next['price']; | |
| $priceCount++; | |
| } | |
| if ($consecutive) { | |
| // We have a full hour | |
| $startFormatted = sprintf("%s:00", $startHour); | |
| $endHour = ($startHour + 1) % 24; | |
| $endFormatted = sprintf("%02d:00", $endHour); | |
| $avgPrice = $priceSum / $priceCount; | |
| $mergedRanges[] = [ | |
| 'start' => $current['start'], | |
| 'end' => $cheapestPeriods[$endIndex]['end'], | |
| 'start_formatted' => $startFormatted, | |
| 'end_formatted' => $endFormatted, | |
| 'avg_price' => $avgPrice, | |
| 'tts_start_formatted' => hourToWords(intval($startHour)), | |
| 'tts_end_formatted' => hourToWords($endHour) | |
| ]; | |
| $i += 4; // Skip the next three periods | |
| continue; | |
| } | |
| } | |
| // If not a full hour, add as single 15-minute period with its price | |
| $startFormatted = $current['start_formatted']; | |
| $endFormatted = $current['end_formatted']; | |
| $ttsStartFormatted = hourToWords(intval($startHour)); | |
| $ttsEndHour = date('H', $current['end']); | |
| $ttsEndMinutes = date('i', $current['end']); | |
| $ttsEndFormatted = $ttsEndMinutes == '00' ? hourToWords(intval($ttsEndHour)) : $current['end_formatted']; | |
| $mergedRanges[] = [ | |
| 'start' => $current['start'], | |
| 'end' => $current['end'], | |
| 'start_formatted' => $startFormatted, | |
| 'end_formatted' => $endFormatted, | |
| 'avg_price' => $current['price'], | |
| 'tts_start_formatted' => $ttsStartFormatted, | |
| 'tts_end_formatted' => $ttsEndFormatted | |
| ]; | |
| $i++; | |
| } | |
| return $mergedRanges; | |
| } | |
| function createChart($timestamps, $prices, $title, $isDarkMode) { | |
| // Chart dimensions | |
| $width = 450; | |
| $height = 270; | |
| $marginLeft = 60; | |
| $marginRight = 20; | |
| $marginTopBottom = 30; | |
| $plotWidth = $width - $marginLeft - $marginRight; | |
| $plotHeight = $height - 2 * $marginTopBottom; | |
| // Create image | |
| $image = imagecreatetruecolor($width, $height); | |
| // Color settings based on dark/light mode | |
| $bgColor = $isDarkMode ? imagecolorallocate($image, 51, 51, 51) : imagecolorallocate($image, 255, 255, 255); | |
| $lineColor = $isDarkMode ? imagecolorallocate($image, 255, 255, 0) : imagecolorallocate($image, 0, 102, 204); | |
| $gridColor = $isDarkMode ? imagecolorallocate($image, 100, 100, 100) : imagecolorallocate($image, 200, 200, 200); | |
| $textColor = $isDarkMode ? imagecolorallocate($image, 200, 200, 200) : imagecolorallocate($image, 0, 0, 0); | |
| // Fill background | |
| imagefill($image, 0, 0, $bgColor); | |
| // Y-axis: Price (cents/kWh) | |
| // Set minPrice to 0 unless there are negative prices | |
| $minPrice = min($prices) < 0 ? min($prices) : 0; | |
| $maxPrice = max($prices); | |
| $priceRange = $maxPrice - $minPrice; | |
| $yScale = $priceRange > 0 ? $plotHeight / $priceRange : 1; | |
| // X-axis: Time periods | |
| $minTime = min(array_column($timestamps, 'start')); | |
| $maxTime = max(array_column($timestamps, 'end')); | |
| $timeRange = $maxTime - $minTime; | |
| $xScale = $timeRange > 0 ? $plotWidth / $timeRange : 1; | |
| // Draw grid lines (Y-axis) | |
| for ($i = 0; $i <= 5; $i++) { | |
| $y = $marginTopBottom + ($plotHeight * $i / 5); | |
| imageline($image, $marginLeft, $y, $width - $marginRight, $y, $gridColor); | |
| } | |
| // Draw X-axis grid lines (every 3 hours) | |
| $startDate = new DateTime(); | |
| $startDate->setTimestamp($minTime); | |
| $startHour = (int)$startDate->format('H'); | |
| $dayStart = clone $startDate; | |
| $dayStart->setTime(0, 0); | |
| for ($i = 0; $i <= 24; $i += 3) { | |
| $time = $dayStart->getTimestamp() + ($i * 3600); | |
| if ($time >= $minTime && $time <= $maxTime) { | |
| $x = $marginLeft + ($time - $minTime) * $xScale; | |
| imageline($image, $x, $marginTopBottom, $x, $height - $marginTopBottom, $gridColor); | |
| } | |
| } | |
| // Draw axes | |
| imageline($image, $marginLeft, $marginTopBottom, $marginLeft, $height - $marginTopBottom, $textColor); | |
| imageline($image, $marginLeft, $height - $marginTopBottom, $width - $marginRight, $height - $marginTopBottom, $textColor); | |
| // Draw Y labels (cents/kWh) | |
| for ($i = 0; $i <= 5; $i++) { | |
| $price = $minPrice + ($priceRange * $i / 5); | |
| $y = $height - $marginTopBottom - ($price - $minPrice) * $yScale; | |
| imagestring($image, 3, 10, $y - 8, number_format($price, 2), $textColor); | |
| } | |
| // Draw X labels (every 3 hours) | |
| for ($i = 0; $i <= 24; $i += 3) { | |
| $time = $dayStart->getTimestamp() + ($i * 3600); | |
| if ($time >= $minTime && $time <= $maxTime) { | |
| $x = $marginLeft + ($time - $minTime) * $xScale; | |
| $label = sprintf('%02d:00', $i); | |
| imagestring($image, 3, $x - 15, $height - $marginTopBottom + 8, $label, $textColor); | |
| } | |
| } | |
| // Draw title | |
| imagestring($image, 4, 10, 5, $title, $textColor); | |
| // Draw line and points | |
| $prevX = null; | |
| $prevY = null; | |
| foreach ($timestamps as $index => $ts) { | |
| $price = $prices[$index]; | |
| $x = $marginLeft + ($ts['start'] - $minTime) * $xScale; | |
| $y = $height - $marginTopBottom - ($price - $minPrice) * $yScale; | |
| // Point | |
| imagefilledellipse($image, $x, $y, 6, 6, $lineColor); | |
| // Line | |
| if ($prevX !== null) { | |
| imageline($image, $prevX, $prevY, $x, $y, $lineColor); | |
| } | |
| $prevX = $x; | |
| $prevY = $y; | |
| } | |
| // Return base64 encoded image | |
| ob_start(); | |
| imagepng($image); | |
| $imageData = ob_get_contents(); | |
| ob_end_clean(); | |
| imagedestroy($image); | |
| return base64_encode($imageData); | |
| } | |
| function generateOnScreenText($timestamps, $prices, $cheapestPeriods) { | |
| $cheapRanges = mergeCheapestHoursIntoRanges($cheapestPeriods, $timestamps, $prices); | |
| $text = "CHEAPEST PERIODS:\n"; | |
| foreach ($cheapRanges as $range) { | |
| $text .= sprintf("%s-%s: %.2f c/kWh\n", $range['start_formatted'], $range['end_formatted'], $range['avg_price']); | |
| } | |
| return $text; | |
| } | |
| function hourToWords($hour) { | |
| $hourWords = [ | |
| 0 => "midnight", 1 => "one", 2 => "two", 3 => "three", 4 => "four", | |
| 5 => "five", 6 => "six", 7 => "seven", 8 => "eight", 9 => "nine", | |
| 10 => "ten", 11 => "eleven", 12 => "noon", 13 => "one", 14 => "two", | |
| 15 => "three", 16 => "four", 17 => "five", 18 => "six", 19 => "seven", | |
| 20 => "eight", 21 => "nine", 22 => "ten", 23 => "eleven" | |
| ]; | |
| $timeOfDay = ""; | |
| if ($hour == 0 || $hour == 12) { | |
| return $hourWords[$hour]; | |
| } elseif ($hour < 12) { | |
| $timeOfDay = " AM"; | |
| } else { | |
| $timeOfDay = " PM"; | |
| } | |
| return $hourWords[$hour] . $timeOfDay; | |
| } | |
| function generateTTSText($cheapestPeriods) { | |
| $cheapRanges = mergeCheapestHoursIntoRanges($cheapestPeriods, [], []); | |
| $rangeTexts = []; | |
| foreach ($cheapRanges as $range) { | |
| $rangeTexts[] = sprintf("%s to %s, %.2f cents per kilowatt-hour", $range['tts_start_formatted'], $range['tts_end_formatted'], $range['avg_price']); | |
| } | |
| return "The cheapest electricity periods today are: " . implode(", ", $rangeTexts); | |
| } | |
| // Main execution | |
| $slides = []; | |
| // Today's data | |
| $todayData = fetchDayAheadPrices(date('Y-m-d')); | |
| if ($todayData) { | |
| $todayProcessed = processPriceData($todayData); | |
| $todayCheapest = findCheapestHours($todayProcessed['timestamps'], $todayProcessed['prices']); | |
| $todayChart = createChart($todayProcessed['timestamps'], $todayProcessed['prices'], "Today's Electricity Prices", $isDarkMode); | |
| $todayOnScreen = generateOnScreenText($todayProcessed['timestamps'], $todayProcessed['prices'], $todayCheapest); | |
| $todayTTS = generateTTSText($todayCheapest); | |
| $slides[] = [ | |
| "title1" => "Today's Electricity Prices", | |
| "title2" => "Today's Electricity Prices", | |
| "title3" => "Prices in cents per kWh", | |
| "onscreen" => $todayOnScreen, | |
| "tts" => $todayTTS, | |
| "img_base64" => $todayChart, | |
| "bell" => true, | |
| "save" => true | |
| ]; | |
| } | |
| // Tomorrow's data | |
| $tomorrowData = fetchDayAheadPrices(date('Y-m-d', strtotime('+1 day'))); | |
| if ($tomorrowData) { | |
| $tomorrowProcessed = processPriceData($tomorrowData); | |
| $tomorrowCheapest = findCheapestHours($tomorrowProcessed['timestamps'], $tomorrowProcessed['prices']); | |
| $tomorrowChart = createChart($tomorrowProcessed['timestamps'], $tomorrowProcessed['prices'], "Tomorrow's Electricity Prices", $isDarkMode); | |
| $tomorrowOnScreen = generateOnScreenText($tomorrowProcessed['timestamps'], $tomorrowProcessed['prices'], $tomorrowCheapest); | |
| $tomorrowTTS = generateTTSText($tomorrowCheapest); | |
| $slides[] = [ | |
| "title1" => "Tomorrow's Electricity Prices", | |
| "title2" => "Tomorrow's Electricity Prices", | |
| "title3" => "Prices in cents per kWh", | |
| "onscreen" => $tomorrowOnScreen, | |
| "tts" => $tomorrowTTS, | |
| "img_base64" => $tomorrowChart, | |
| "bell" => true, | |
| "save" => true | |
| ]; | |
| } | |
| // Output JSON for slideshow | |
| header('Content-Type: application/json'); | |
| echo json_encode($slides); | |
| ?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment