• compass-1130x200.elsinga.net.jpg
  • robert_scouting.jpg
  • homey.jpg
  • hiking_silhouette.jpg
  • route.jpg
  • dashcam.jpg

Dynamische energieprijzen volgen - klik om te vergroten

Met deze Homey advanced flow volg ik de dynamische kwartierprijzen (bron: Power by the Hour NL 15min prijzen) en toon deze op diverse manieren op mijn AWTRIX display(s):

  • de huidige prijs in Euro's in een kleur die past bij de *relatieve prijs* binnen de dynamische tarieven van deze dag. Groen als de dynamische prijs in het onderste kwart van de spread van vandaag valt, rood in het bovenste kwart en geel in de rest. Hierdoor kan ik zien of stroom gebruiken op dat moment relatief goedkoop of duur is. Bij inkoop onder nul is de prijs blauw en bij inclusief EB/BTW onder nul paars, ongeacht de relatieve prijs.
  • een gekleurde indicator in de rechter onderhoek, die dezelfde kleuren gebruikt, maar die ik wellicht ga omzetten naar *absolute prijzen*, zodat ik ook weet of de getoonde kwartierprijs absoluut goedkoop of duur is
  • een grafiek van de huidige en komende *half uur* (want maar 32 kolommen) prijzen in absolute prijzen. Rood bij >€0.35, geel €0.25-0.35, groen €0.132-0.25, blauw €0.0-0.131 en paars <€0.0. Als er nog geen Power by the Hour prijzen voor morgen beschikbaar zijn, dan haalt het script dat de grafiek maakt de voorspelde prijzen voor morgen op bij https://www.energyforecast.de en toont deze in minder felle kleuren, om het verschil met definitieve prijzen aan te geven. Als de voorspelde prijzen niet beschikbaar zijn wordt dat deel van de grafiek leeg gelaten. Als extra wordt bij de eerste kolom met prijzen van morgen een witte pixel bovenaan getoond, zodat de dagovergang goed zichtbaar is.

Dit ziet er dan zo uit:

awtrix

Voor het tonen van de grafiek moet in het Homeyscript onderaan het IP adres van je AWTRIX en de aan te maken API key vaan energyforecats.de worden ingevuld bovenaan. 

Voor deze flow worden de volgende variabelen gebruikt:
- (#) Gemiddelde energieprijs vandaag - volgens Power by the Hour
- (#) LaagstePrijsVandaag - PbtH
- (#) HoogstePrijsVandaag - PbtH
- (#) EPrijsSread - de spread tussen hoogste en laagste prijs
- (#) EPlaagcutoff - onder deze prijs relatief goedkoop
- (#) EPhoogcutoff - boven deze prijs relatief duur

- (#) HuidigeEnergieprijs - de daadwerkelijke kwartierprijs
- (a) Relatieve_Energieprijs - Gratis, Zeer laag, Laag, Middel, Hoog, Zeer Hoog (volgt de kleuren die hierboven zijn genoemd)

- (a) Grafiek_PbtH_Vandaag - de PbtH JSON met daarin alle 96 kwartieprijzen van vandaag
- (a) Grafiek_PbtH_Morgen - idem voor morgen, leeg als geen prijzen beschikbaar

De Awtrix energie grafiek Homeyscript:

// Awtrix Energie Grafiek (32 Half-uren, Dim-kleuren voor voorspelling, Middernacht-Markering)
const awtrixIP = "192.168.1.100";
const apiToken = "xxxxxxxxx"; // API token van https://www.energyforecast.de
const fixedCostCent = 11.2; // vaste kosten zoals Energiebelasting en inkoopvergoeding, ex BTW
const forecastUrl = `https://www.energyforecast.de/api/v1/predictions/next_48_hours?market_zone=NL&resolution=QUARTER_HOURLY&vat=21&fixed_cost_cent=${fixedCostCent}`;

const logicVars = await Homey.logic.getVariables();
let todayStr = "", tomorrowStr = "";
for (let id in logicVars) {
let name = logicVars[id].name.toLowerCase();
if (name === "grafiek_pbth_vandaag") todayStr = logicVars[id].value;
if (name === "grafiek_pbth_morgen") tomorrowStr = logicVars[id].value;
}

function parsePrices(str) {
if (!str || str === "") return [];
return str.replace(/[\[\]\s]/g, '').split(',').map(p => parseFloat(p)).filter(p => !isNaN(p));
}

function toHalfHourly(prices) {
let halfHourly = [];
for (let i = 0; i < prices.length; i += 2) {
let chunk = prices.slice(i, i + 2);
if (chunk.length > 0) {
halfHourly.push(chunk.reduce((sum, val) => sum + val, 0) / chunk.length);
}
}
return halfHourly;
}

let todayPbtH = toHalfHourly(parsePrices(todayStr));
let tomorrowPbtH = toHalfHourly(parsePrices(tomorrowStr));

const nowUtc = new Date();
const now = new Date(nowUtc.toLocaleString("en-US", {timeZone: "Europe/Amsterdam"}));
const currentHalfHourIndex = (now.getHours() * 2) + (now.getMinutes() >= 30 ? 1 : 0);

// Bepaal hoeveel uren er nog van 'vandaag' in de grafiek passen
let todayRemaining = todayPbtH.slice(currentHalfHourIndex);
let todayRemainingCount = todayRemaining.length;

let knownPrices = todayRemaining.concat(tomorrowPbtH);
let debugMsg = `PbtH heeft ${knownPrices.length} half-uren. `;

let forecastPrices = [];
if (knownPrices.length < 32) {
try {
const response = await fetch(forecastUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Accept': 'application/json'
}
});

if (response.ok) {
const forecastJson = await response.json();
if (forecastJson.data) {
const currentHalfHourMs = Math.floor(Date.now() / 1800000) * 1800000;
const missingTimeMs = currentHalfHourMs + (knownPrices.length * 1800000);

let apiPrices = [];
for (let i = 0; i < forecastJson.data.length; i += 2) {
let chunk = forecastJson.data.slice(i, i + 2);
if (chunk.length > 0) {
let avg = chunk.reduce((s, val) => s + (val.total_ct_kwh / 100), 0) / chunk.length;
let timeMs = new Date(chunk[0].start).getTime();
apiPrices.push({ timeMs, price: avg });
}
}

forecastPrices = apiPrices.filter(ap => ap.timeMs >= missingTimeMs).map(ap => ap.price);
debugMsg += `API gaf ${apiPrices.length}, ${forecastPrices.length} bruikbaar. `;
}
} else {
debugMsg += `API Fout: ${response.status}. `;
}
} catch (e) {
debugMsg += `Netwerkfout API. `;
}
}

let targetData = [];
let forecastIndex = 0;

for (let i = 0; i < 32; i++) {
let isFirstOfTomorrow = (i === todayRemainingCount);

if (i < knownPrices.length) {
targetData.push({ price: knownPrices[i], isForecast: false, isTomorrowStart: isFirstOfTomorrow });
} else if (forecastIndex < forecastPrices.length) {
targetData.push({ price: forecastPrices[forecastIndex], isForecast: true, isTomorrowStart: isFirstOfTomorrow });
forecastIndex++;
} else {
break;
}
}

if (targetData.length === 0) return "Geen data gevonden";

const allPrices = targetData.map(d => d.price);
const minPrice = Math.min(...allPrices);
const maxPrice = Math.max(...allPrices);
const range = (maxPrice - minPrice) || 1;

let y_zero = 8;
if (minPrice < 0) {
const zero_norm = (0 - minPrice) / range;
y_zero = Math.round(7 - (zero_norm * 7));
y_zero = maxPrice > 0 ? Math.max(1, Math.min(7, y_zero)) : Math.max(0, Math.min(7, y_zero));
}

const drawCommands = [];
let x = 0;

targetData.forEach(item => {
let price = item.price;
let norm = (price - minPrice) / range;
let y_target = Math.round(7 - (norm * 7));

// Bepaal de felle kleur en de donkere (dim) variant
let color = "#0000FF";
let dimColor = "#000044";

if (price > 0.35) {
color = "#FF0000";
dimColor = "#440000";
} else if (price >= 0.25) {
color = "#FFFF00";
dimColor = "#444400";
} else if (price >= 0.132) {
color = "#00FF00";
dimColor = "#004400";
} else if (price < 0) {
color = "#9900FF";
dimColor = "#330055";
}

// Kies de actieve kleur op basis van de isForecast status
let activeColor = item.isForecast ? dimColor : color;

// Teken de massieve balk
if (price >= 0) {
let h = (minPrice >= 0) ? Math.max(1, Math.round(norm * 7) + 1) : Math.max(1, y_zero - y_target);
let start_y = (minPrice >= 0) ? 7 : y_zero - 1;

drawCommands.push({ "df": [x, start_y - h + 1, 1, h, activeColor] });
} else {
let h = Math.max(1, y_target - y_zero + 1);
drawCommands.push({ "df": [x, y_zero, 1, h, activeColor] });
}

// Teken de witte middernacht-pixel op de bovenste rij (y=0)
if (item.isTomorrowStart) {
drawCommands.push({ "dp": [x, 0, "#FFFFFF"] });
}

x += 1;
});

const payload = { "text": " ", "draw": drawCommands, "duration": 10 };

try {
const response = await fetch(`http://${awtrixIP}/api/custom?name=energie`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return response.ok ? debugMsg + "Grafiek (Dim + Witte Pixel) verstuurd!" : "Fout bij Awtrix API";
} catch (error) {
return "Netwerkfout bij versturen naar Awtrix";
}