Skip to content

Instantly share code, notes, and snippets.

@developerfromjokela
Last active October 17, 2025 19:28
Show Gist options
  • Select an option

  • Save developerfromjokela/edbc943aa8ebfc0f4e9d20a70f1af754 to your computer and use it in GitHub Desktop.

Select an option

Save developerfromjokela/edbc943aa8ebfc0f4e9d20a70f1af754 to your computer and use it in GitHub Desktop.
Nordpool market price for OpenCARWINGS
<?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.'&currency=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