Homey's eigen energieprijzen via HomeyScript

Pagina: 1
Acties:

Vraag


Acties:
  • +5 Henk 'm!

  • TKroon
  • Registratie: December 2006
  • Niet online
Omdat het te groot is om te posten in het Homey-forum, deel ik het hier :)

Dit jaar heeft Power By The Hour me al meerdere malen in de steek gelaten en geen energieprijzen opgehaald. Er is geen mogelijkheid om dit te forceren, met als gevolg dat alle prijsgestuurde flows (warmtepomp, EV, etc) niet meer werken. Recent heeft Athom zelf ook het een en ander toegevoegd, maar dit is niet uitgebreid en flexibel genoeg. Daar komt nog bij dat de energieprijzen om 13:00 al beschikbaar zijn, en met name PbtH vaak pas na 15:00 de prijzen heeft, waardoor de auto nog 20 kWh heeft geladen terwijl de prijzen de dag erna flink lager waren. Dus... tijd om het zelf te doen. Of ja, met behulp van ChatGPT.

Allereerst heb ik een tekstuele variabele aangemaakt waar ik de JSON opsla. Een voor vandaag en een voor morgen. Met dit script haal je het ID van de variabele op. Die heb je later nodig.

code:
1
2
3
let variables = await Homey.logic.getVariables(); //use this to get the ID of any Logic Variable
console.log('Variables:', variables); //use this to get the ID of any Logic Variable
return variables; //use this to get the ID of any Logic Variable


De JSON bevat tegenwoordig kwartierwaarden, maar Zonneplan werkt met uurwaarden. Dit is letterlijk het gemiddelde van de betreffende vier uurwaarden. Ook wil ik de data van UTC tijden naar NL-tijd. Dus uur 0 = tijdvak 00:00-01:00.
Elke middag wordt om 13:05 dit script gedraaid, met nog enkele checks later op de dag of er data is.
Elke nacht om 0:00 wordt de JSON van morgen naar de JSON van vandaag gezet, en die van morgen leeggemaakt.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// ===== Config =====
const DELIVERY_AREA = "NL";
const CURRENCY = "EUR";
const BTW_FACTOR = 1.21;
const OPSLAG = 0.14278;
// ==================

// Datum van morgen in lokale tijd
let now = new Date();
now.setDate(now.getDate() + 1);
now.setHours(0, 0, 0, 0);
let yyyy = now.getFullYear();
let mm = String(now.getMonth() + 1).padStart(2, '0');
let dd = String(now.getDate()).padStart(2, '0');
let tomorrowStr = `${yyyy}-${mm}-${dd}`;

// API ophalen
let url = `https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices?date=${tomorrowStr}&market=DayAhead&deliveryArea=${DELIVERY_AREA}&currency=${CURRENCY}`;
let response = await fetch(url);
if (!response.ok) throw new Error(`HTTP-fout ${response.status}`);

// Controle op lege of ongeldige JSON
let text = await response.text();
if (!text || text.trim().length === 0) {
  throw new Error("Lege respons van Nordpool – prijzen nog niet beschikbaar");
}

let data;
try {
  data = JSON.parse(text);
} catch {
  throw new Error("Kon JSON niet parsen – waarschijnlijk onvolledige data");
}

if (!data.multiAreaEntries) {
  throw new Error("Ongeldige API-structuur – prijzen nog niet gepubliceerd");
}

// === Controle of data wel echt voor morgen is ===
let datesInData = data.multiAreaEntries.map(e => e.deliveryStart.split("T")[0]);
let uniqueDates = [...new Set(datesInData)];

if (!uniqueDates.includes(tomorrowStr)) {
  // Data bevat niet de verwachte datum
  await Homey.logic.updateVariable({
    id: "8eaf420a-2749-41ec-8be3-d1ea789ef2df",
    variable: { value: "error" }
  });
  return "error";
}

// Filter alle entries voor NL
let entries = data.multiAreaEntries.filter(e => e.entryPerArea[DELIVERY_AREA] !== undefined);

// Groeperen per lokale uur
let hourlyPrices = Array.from({ length: 24 }, () => []);
entries.forEach(e => {
  let start = new Date(e.deliveryStart);
  let localHour = start.toLocaleString("nl-NL", {
    hour: "2-digit",
    hour12: false,
    timeZone: "Europe/Amsterdam"
  });
  localHour = parseInt(localHour, 10);
  if (localHour >= 0 && localHour < 24) {
    hourlyPrices[localHour].push(e.entryPerArea[DELIVERY_AREA] / 1000); // €/MWh → €/kWh
  }
});

// Gemiddelde per uur berekenen en all-in prijs
let hourData = hourlyPrices.map((hourArray, index) => {
  if (hourArray.length === 0) return { hour: index, price: null };
  let avg = hourArray.reduce((sum, p) => sum + p, 0) / hourArray.length;
  return { hour: index, price: Number(((avg * BTW_FACTOR) + OPSLAG).toFixed(4)) };
});

// Datum toevoegen aan elk uur
let hourDataWithDate = hourData.map(h => ({
  date: tomorrowStr,
  hour: h.hour,
  price: h.price
}));

// Homey variabele bijwerken
await Homey.logic.updateVariable({
  id: "8eaf420a-2749-41ec-8be3-d1ea789ef2df",
  variable: { value: JSON.stringify(hourDataWithDate) }
});

return hourDataWithDate;


Oké, nu heeft Homey alle prijzen, opgeslagen in variabelen met ID's. Ik heb een advanced virtual device aangemaakt met de waarden die ik wil hebben. Die heb ik nodig zodat het laatste script alles erin op kan slaan. Maar eerst hebben we weer het ID nodig van het AVD, en de nummers van de waarden die je gebruikt. Het ID is het resultaat wat je in onderstaand script invoert in targetDeviceId. Ga naar https://tools.developer.homey.app/tools/devices en zoek naar je AVD.

Afbeeldingslocatie: https://tweakers.net/i/Mos4aGfZiWpU18SEAq2W-ib3KyI=/800x/filters:strip_exif()/f/image/2817QaEY21cOW1vNJlE080G1.png?f=fotoalbum_large

Hierin zie je je ID, en tevens de namen van de nummers die je gebruikt. Tot slot draai je onderstaand script elk uur:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/**
 * stroomprijzen_fixed.js
 * Zelfde functionaliteit als eerder, maar fixed: gebruik lokaal uur voor huidige match
 */

const vandaagId      = 'ebeb6976-a47a-4086-bd40-25d9df2a2306';
const morgenId       = '8eaf420a-2749-41ec-8be3-d1ea789ef2df';
const targetDeviceId = '465e6417-b002-4246-bc7f-0760faeb2b93';
const TZ             = 'Europe/Amsterdam';

const capabilities = {
  vandaag: {
    actuelePrijs: 'measure_devicecapabilities_number-custom_12.number1',
    laagstePrijs: 'measure_devicecapabilities_number-custom_11.number2',
    laagsteUur:   'measure_devicecapabilities_number-custom_61.number3',
    gemiddelde:   'measure_devicecapabilities_number-custom_12.number4',
    plus1:        'measure_devicecapabilities_number-custom_12.number8',
    plus2:        'measure_devicecapabilities_number-custom_12.number9',
    plus3:        'measure_devicecapabilities_number-custom_12.number10',
    plus4:        'measure_devicecapabilities_number-custom_12.number11',
    plus5:        'measure_devicecapabilities_number-custom_12.number12',
    plus6:        'measure_devicecapabilities_number-custom_12.number13',
    plus7:        'measure_devicecapabilities_number-custom_12.number14',
    plus8:        'measure_devicecapabilities_number-custom_12.number15',
    gem8:         'measure_devicecapabilities_number-custom_12.number16',
    laag8:        'measure_devicecapabilities_number-custom_11.number17',
    laag24:       'measure_devicecapabilities_number-custom_11.number18',
    gem24:        'measure_devicecapabilities_number-custom_12.number19',
    laagToekomst: 'measure_devicecapabilities_number-custom_11.number20',
    gemToekomst:  'measure_devicecapabilities_number-custom_12.number21',
  },
  morgen: {
    laagstePrijs: 'measure_devicecapabilities_number-custom_11.number5',
    laagsteUur:   'measure_devicecapabilities_number-custom_61.number6',
    gemiddelde:   'measure_devicecapabilities_number-custom_12.number7',
  },
};

// HELPERS
function getDateHourInTZ(date = new Date(), timeZone = TZ) {
  const fmt = new Intl.DateTimeFormat('en-CA', {
    timeZone,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', hour12: false,
  });
  const parts = fmt.formatToParts(date);
  const get = t => parts.find(p => p.type === t)?.value;
  return { localDate: `${get('year')}-${get('month')}-${get('day')}`, localHour: Number(get('hour')) };
}

function round3(v) { return Math.round(Number(v) * 1000) / 1000; }

function safeStats(values) {
  const nums = (values || []).map(Number).filter(v => Number.isFinite(v));
  if (!nums.length) return { min: 999, avg: 999 };
  const min = Math.min(...nums);
  const avg = nums.reduce((a, b) => a + b, 0) / nums.length;
  return { min, avg };
}

function parsePrices(raw) {
  if (!Array.isArray(raw)) return [];
  return raw
    .map(p => ({ date: String(p.date), hour: Number(p.hour), price: Number(p.price) }))
    .filter(p => Number.isFinite(p.hour) && Number.isFinite(p.price));
}

function sortPrices(prices) {
  return prices.slice().sort((a, b) => {
    if (a.date !== b.date) return a.date < b.date ? -1 : 1;
    return a.hour - b.hour;
  });
}

// === main ===
const now = new Date();
const { localDate, localHour } = getDateHourInTZ(now, TZ);

// Load vandaag
let vandaagVar;
try {
  vandaagVar = await Homey.logic.getVariable({ id: vandaagId });
} catch (e) {
  return `❌ ERROR: Kon Logic-variabele vandaag niet ophalen: ${e.message || e}`;
}
if (!vandaagVar?.value) return `❌ ERROR: Logic-variabele vandaag heeft geen waarde`;

let vandaagRaw;
try {
  vandaagRaw = typeof vandaagVar.value === 'string' ? JSON.parse(vandaagVar.value) : vandaagVar.value;
} catch (e) {
  return `❌ ERROR: Kon JSON van vandaag niet parsen: ${e.message || e}`;
}
let vandaagPrices = parsePrices(vandaagRaw);
if (!vandaagPrices.length) return `❌ ERROR: Vandaag-JSON bevat geen bruikbare entries`;

// Load morgen (optioneel)
let morgenPrices = [];
try {
  const morgenVar = await Homey.logic.getVariable({ id: morgenId });
  if (morgenVar?.value) {
    const morgenRaw = typeof morgenVar.value === 'string' ? JSON.parse(morgenVar.value) : morgenVar.value;
    morgenPrices = parsePrices(morgenRaw);
  }
} catch (e) {
  console.log(`[WARN] Morgenprijzen konden niet geladen worden: ${e.message || e}`);
}

// Combineer en sorteer
let combinedPrices = sortPrices([...vandaagPrices, ...morgenPrices]);

// Bepaal index voor 'nu' — gebruik LOKAAL uur en prefer today's date when possible
let idxNow = combinedPrices.findIndex(p => p.date === localDate && p.hour === localHour);
if (idxNow === -1) {
  // probeer volgend uur op dezelfde datum
  idxNow = combinedPrices.findIndex(p => p.date === localDate && p.hour >= localHour);
}
if (idxNow === -1) {
  // fallback: eerste toekomstige entry (eerste met datum >= vandaag)
  idxNow = combinedPrices.findIndex(p => {
    return p.date > localDate || (p.date === localDate && p.hour >= localHour);
  });
}
if (idxNow === -1) {
  // laatste fallback: eerste entry
  idxNow = 0;
  console.log(`[WARN] Kon geen passende 'nu' entry vinden; fallback naar index 0`);
}

// Huidige en +1..+8 prijzen
const getPriceAt = (i) => combinedPrices[idxNow + i]?.price ?? 999;
const currentPrice = getPriceAt(0);
const prijsPlus = Array.from({ length: 8 }, (_, i) => getPriceAt(i + 1));

// Statistieken komende N uren (neem alleen bestaande entries)
function statsNext(n) {
  const slice = combinedPrices.slice(idxNow, idxNow + n).map(p => p.price);
  return safeStats(slice);
}
const stat8 = statsNext(8);
const stat24 = statsNext(24);
const statFuture = safeStats(combinedPrices.slice(idxNow).map(p => p.price));

// Vandaag statistieken (gewoon op vandaagPrices)
const statVandaag = safeStats(vandaagPrices.map(p => p.price));
const recLaagsteVandaag = vandaagPrices.find(p => p.price === statVandaag.min);
const laagsteUurVandaagLocal = (recLaagsteVandaag?.hour ?? 999);

// Bereid capability updates
const updates = [
  { id: capabilities.vandaag.actuelePrijs, value: round3(currentPrice), label: 'Actuele prijs' },
  { id: capabilities.vandaag.plus1, value: round3(prijsPlus[0]), label: '+1u' },
  { id: capabilities.vandaag.plus2, value: round3(prijsPlus[1]), label: '+2u' },
  { id: capabilities.vandaag.plus3, value: round3(prijsPlus[2]), label: '+3u' },
  { id: capabilities.vandaag.plus4, value: round3(prijsPlus[3]), label: '+4u' },
  { id: capabilities.vandaag.plus5, value: round3(prijsPlus[4]), label: '+5u' },
  { id: capabilities.vandaag.plus6, value: round3(prijsPlus[5]), label: '+6u' },
  { id: capabilities.vandaag.plus7, value: round3(prijsPlus[6]), label: '+7u' },
  { id: capabilities.vandaag.plus8, value: round3(prijsPlus[7]), label: '+8u' },
  { id: capabilities.vandaag.gem8, value: round3(stat8.avg), label: 'Gemiddelde komende 8u' },
  { id: capabilities.vandaag.laag8, value: round3(stat8.min), label: 'Laagste komende 8u' },
  { id: capabilities.vandaag.gem24, value: round3(stat24.avg), label: 'Gemiddelde komende 24u' },
  { id: capabilities.vandaag.laag24, value: round3(stat24.min), label: 'Laagste komende 24u' },
  { id: capabilities.vandaag.gemToekomst, value: round3(statFuture.avg), label: 'Gemiddelde toekomst' },
  { id: capabilities.vandaag.laagToekomst, value: round3(statFuture.min), label: 'Laagste toekomst' },
  { id: capabilities.vandaag.laagstePrijs, value: round3(statVandaag.min), label: 'Laagste prijs vandaag' },
  { id: capabilities.vandaag.laagsteUur, value: round3(laagsteUurVandaagLocal), label: 'Laagste uur vandaag (lokaal)' },
  { id: capabilities.vandaag.gemiddelde, value: round3(statVandaag.avg), label: 'Gemiddelde vandaag' },
  // Morgen (zoals voorheen)
  { id: capabilities.morgen.laagstePrijs, value: round3(safeStats(morgenPrices.map(p => p.price)).min), label: 'Laagste prijs morgen' },
  { id: capabilities.morgen.laagsteUur,   value: round3((morgenPrices.find(p => p.price === safeStats(morgenPrices.map(p => p.price)).min)?.hour) ?? 999), label: 'Laagste uur morgen (lokaal)' },
  { id: capabilities.morgen.gemiddelde,   value: round3(safeStats(morgenPrices.map(p => p.price)).avg), label: 'Gemiddelde morgen' },
];

// Zet capabilities
for (const cap of updates) {
  try {
    await Homey.devices.setCapabilityValue({ deviceId: targetDeviceId, capabilityId: cap.id, value: cap.value });
    console.log(`✅ ${cap.label}: ${cap.value} (${cap.id})`);
  } catch (e) {
    console.log(`❌ Fout bij ${cap.label} (${cap.id}): ${e.message || e}`);
  }
}


En dit is het resultaat!
Afbeeldingslocatie: https://tweakers.net/i/uNg8Injs1oFqTa1g2085V-wHfPM=/x800/filters:strip_exif()/f/image/Nv5OfdxwS1p8jXUH1uFVVqDM.png?f=fotoalbum_large

Daikin Altherma 3 LT 8 kW + 14,2 kWp PV

Alle reacties


Acties:
  • 0 Henk 'm!

  • PetraF
  • Registratie: April 2023
  • Laatst online: 21-10 18:15
Ik ervaar hetzelfde probleem met Power by the Hour en krijg op moment van van spreken geen energieprijzen door. Echter heb ik een Homey Pro 2023 en weet niet of ik jouw codes kan gebruiken?

Acties:
  • 0 Henk 'm!

  • TKroon
  • Registratie: December 2006
  • Niet online
@PetraF Ik heb ook een Pro 2023. Je moet de app HomeyScript hebben, en ik gebruik dus ook de advanced virtual devices. Je dient zoals hierboven beschreven de ID's en capability-namen erbij te zoeken van je AVD.

Daikin Altherma 3 LT 8 kW + 14,2 kWp PV


Acties:
  • 0 Henk 'm!

  • Aziraphale
  • Registratie: September 2013
  • Nu online
Maar krijgt deze dan wel de prijzen goed binnen?

3120WP Solax, 4825WP SolarEdge, Nibe S2125-8 met een VVMS320, 11,52 kWh Zendure 2400AC


Acties:
  • +1 Henk 'm!

  • TKroon
  • Registratie: December 2006
  • Niet online
@Aziraphale ja, ik had gisteren al gewoon de prijzen van vandaag. Hij haalt ze direct bij Nordpool op en in mijn flows herhaalt hij het script indien nodig. En niet zoals bij PbtH die het eenmalig probeert, of Homey zelf die ook nog altijd niet de prijzen van vandaag heeft. Tot slot heb je hierbij zelfs nog de mogelijkheid de prijzen handmatig in te voeren, wat met PbtH en Homey zelf ook niet kan.

Daikin Altherma 3 LT 8 kW + 14,2 kWp PV