Resultaten van Zonneplan doorsturen vanuit Homey
Heb je een Homey smart home systeem en wil je jouw resultaten live delen op deze site zodat we de opbrengst kunnen vergelijken? Meld je dan aan, voeg je installatie toe en log in. Op je profielpagina staat jouw persoonlijke API-key, hiermee kun je de volgende stappen volgen.
Wanneer je je gegevens deelt volgens onderstaande stappenplan dan worden een aantal gegevens vanuit de (onofficiële) Homey-Zonneplan integratie doorgestuurd naar deze website.
Configuratie in Homey
We nemen aan dat je een Homey account en een Homey of Homey Pro hebt.
- Installeer Homeyscript, hiermee kun je scripts installeren op je Homey.
- Installeer Zonneplan. Momenteel zijn de benodigde statistieken alleen beschikbaar met de test versie van de app beschikbaar
- Stel de koppeling met Zonneplan in en verifieer dat je de metingen van de Nexus in Homey kunt zien.
- Ga in je Homey naar de HomeyScript applicatie en plak daar het onderstaande script. Dit script zoekt de eerste Zonneplan Nexus batterij op, slaat het id op voor volgende runs en zal de gegevens doorsturen naar Onbalansmarkt zodra de 'last measurement time' is veranderd sinds de vorige executie.
- Vul in het bestand hieronder op regel 1 jouw Mijnbatterij API key in, vervang hierbij API-KEY-MIJNBATTERIJ.NL voor jouw Mijnbatterij API key en sla het script op.
- Klik op de ‘Test’ knop om te controleren of het script succesvol uitgevoerd wordt.
- Maak een nieuwe Flow aan die elke minuut het script uitvoert
/**
* Zonneplan -> Mijnbatterij data synchronisatie via de Zonneplan plugin op Homey.
*
* Dit script leest de waarden van jouw Nexus batterij in Homey uit.
*
* Gebruik:
* - Maak een nieuw HomeyScript aan voor dit script.
* - Maak een nieuwe (standaard) Homey flow:
* - Als: Elke X minuten, bijvoorbeeld elke 7 minuten. (Zonneplan update elke 5 minuten dus vaker dan dat is niet nodig).
* - Dan: Voer script uit (kies het zojuist aangemaakte script).
*
* Zodra het script wordt uitgevoerd:
* - Sturen we de live data door (zolang er een nieuwe meting beschikbaar is in de Homey plugin)
* - Na 11:00 sturen we eenmalig het totaal van deze maand in en aan het begin van de maand ook het resultaat van vorige maand.
*
* BELANGRIJK: dit script werkt alleen met de officiele Zonneplan plugin
* (https://homey.app/nl-nl/app/nl.zonneplan/Zonneplan/). De oudere community
* versie van Bloemendaal (driverId begint met 'homey:app:app.bloemendaal.zonneplan')
* wordt NIET ondersteund.
*/
// Vul hieronder jouw API key in (tussen enkele quotes!):
const MB_KEY = 'JOUW-API-KEY-VAN-MIJNBATTERIJ';
// overige constanten
const TZ = 'Europe/Amsterdam';
const MB_BASE = 'https://api.mijnbatterij.nl/api';
// Driver-id prefix van de niet-ondersteunde Bloemendaal community plugin.
const UNSUPPORTED_DRIVER_PREFIX = 'homey:app:app.bloemendaal.zonneplan';
const SUPPORTED_PLUGIN_URL = 'https://homey.app/nl-nl/app/nl.zonneplan/Zonneplan/';
// ── Mijnbatterij API ──────────────────────────────────────────────────────────
class MijnbatterijApi {
async post(path, payload) {
log(`MB → ${path}: ${JSON.stringify(payload)}`);
const res = await fetch(`${MB_BASE}${path}`, {
method: 'POST',
headers: { Authorization: `Bearer ${MB_KEY}`, 'Content-Type': 'application/json', 'User-Agent': 'Homey-ZP3' },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error(`MB POST ${path} → ${res.status}`);
}
sendLive(p) { return this.post('/live', p); }
sendMonthly(p) { return this.post('/results/monthly', p); }
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function localNow() {
// sv-SE locale formats as "YYYY-MM-DD HH:MM" naturally
const s = new Intl.DateTimeFormat('sv-SE', {
timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false
}).format(new Date());
const [date, time] = s.split(' ');
const [year, month, day] = date.split('-');
return { year, month, day, hour: +time.slice(0, 2), dateStr: date, monthStr: date.slice(0, 7) };
}
function tzOffsetHours() {
const parts = new Intl.DateTimeFormat('en-US', { timeZone: TZ, timeZoneName: 'short' })
.formatToParts(new Date());
const m = parts.find(p => p.type === 'timeZoneName').value.match(/GMT([+-]\d{1,2}):?(\d{2})?/);
return m ? +m[1] + (m[2] ? +m[2] / 60 : 0) : 0;
}
function parseMeasuredAt(raw) {
// raw: "DD-MM-YYYY HH:MM" in Amsterdam local time → UTC Date
const [d, mo, y, h, mi] = raw.match(/\d+/g);
return new Date(new Date(`${y}-${mo}-${d}T${h}:${mi}:00Z`) - tzOffsetHours() * 3600000);
}
function prevMonthStr(year, month) {
const d = new Date(`${year}-${month}-01T00:00:00Z`);
d.setUTCMonth(d.getUTCMonth() - 1);
return d.toISOString().slice(0, 7); // YYYY-MM of previous month
}
// ── Plugin compatibiliteit ────────────────────────────────────────────────────
function assertSupportedPlugin(battery) {
const driverId = (battery.driverId || '').toLowerCase();
if (driverId.startsWith(UNSUPPORTED_DRIVER_PREFIX)) {
const msg =
'❌ Niet-ondersteunde Zonneplan plugin gedetecteerd.\n' +
` Gevonden driverId: ${battery.driverId}\n` +
' Het script is niet geschikt voor deze versie.\n' +
'\n' +
' Oplossing:\n' +
' 1) Verwijder de huidige Zonneplan app uit Homey.\n' +
' 2) Installeer de Zonneplan plugin via:\n' +
` ${SUPPORTED_PLUGIN_URL}\n` +
' 3) Voeg je batterij opnieuw toe en draai dit script nogmaals.';
log(msg);
throw new Error('Niet-ondersteunde Zonneplan plugin (Bloemendaal). Installeer de officiele plugin: ' + SUPPORTED_PLUGIN_URL);
}
}
// ── Battery device ────────────────────────────────────────────────────────────
async function getBattery() {
const storedId = global.get('zp-battery-id');
if (storedId) {
try {
const dev = await Homey.devices.getDevice({ id: storedId });
// Een eerder opgeslagen id kan nog naar de oude plugin verwijzen — dat
// willen we niet stilzwijgend hergebruiken.
if ((dev.driverId || '').toLowerCase().startsWith(UNSUPPORTED_DRIVER_PREFIX)) {
log('Opgeslagen batterij id verwijst naar de niet-ondersteunde Bloemendaal plugin — opnieuw zoeken');
global.set('zp-battery-id', null);
} else {
return dev;
}
} catch (_) {}
}
const devices = await Homey.devices.getDevices();
const all = Object.values(devices);
// Eerst: is er uberhaupt een Zonneplan-batterij van de officiele plugin?
const supported = all.find(
d => d.class === 'battery' &&
(d.driverId || '').toLowerCase().includes('zonneplan') &&
!(d.driverId || '').toLowerCase().startsWith(UNSUPPORTED_DRIVER_PREFIX)
);
if (supported) {
global.set('zp-battery-id', supported.id);
log(`Batterij id ${supported.id} opgeslagen (driverId ${supported.driverId})`);
return supported;
}
// Niets gevonden van de officiele plugin: kijk of de gebruiker juist de oude
// Bloemendaal versie heeft draaien — dan kunnen we een gerichte foutmelding geven.
const unsupported = all.find(
d => d.class === 'battery' &&
(d.driverId || '').toLowerCase().startsWith(UNSUPPORTED_DRIVER_PREFIX)
);
if (unsupported) {
assertSupportedPlugin(unsupported);
}
throw new Error('Geen Zonneplan batterij gevonden');
}
// ── Sync routines ─────────────────────────────────────────────────────────────
async function syncLive(cap, mb) {
const raw = cap['lastmeasured']?.value;
if (!raw || raw === 'undefined') return log('Live: geen lastmeasured waarde');
const measurementTime = parseMeasuredAt(raw);
const stored = global.get('zp-last-measured');
if (stored && new Date(stored) >= measurementTime) return log('Live: geen nieuwe data');
const selfConsumption = cap['boolean.selfconsumptionactive']?.value;
const homeOptimization = cap['boolean.homeoptimizationactive']?.value;
const mode = homeOptimization ? 'self_consumption_plus' : selfConsumption ? 'self_consumption' : undefined;
const imp = cap['meter_power.daily_import']?.value;
const exp = cap['meter_power.daily_export']?.value;
await mb.sendLive({
timestamp: measurementTime.toISOString(),
batteryResult: String(cap['meter_power.daily_earned']?.value ?? 0),
batteryResultTotal: String(cap['meter_power.total_earned']?.value ?? 0),
batteryCharge: String(cap['measure_battery']?.value ?? 0),
loadBalancingActive: cap['boolean.dynamicloadbalancingactive']?.value ? 'on' : 'off',
...(cap['cycle_count']?.value != null && { totalBatteryCycles: String(cap['cycle_count'].value) }),
...(imp != null && imp !== 'undefined' && { chargedToday: String(Math.round(imp)) }),
...(exp != null && exp !== 'undefined' && { dischargedToday: String(Math.round(exp)) }),
...(mode && { mode }),
});
global.set('zp-last-measured', measurementTime.toISOString());
log(`Live: verstuurd voor ${measurementTime.toISOString()}`);
}
async function syncDaily(cap, mb, now) {
if (now.hour < 11) return;
if (global.get('zp-last-daily-date') === now.dateStr) return;
// Current month total — capability already stores euros (plugin divides raw value by 10^7)
const thisMonthResult = cap['meter_power.total_earned_thismonth']?.value;
if (thisMonthResult != null) {
await mb.sendMonthly({ yearMonth: now.monthStr, batteryResult: String(thisMonthResult) });
log(`Daily: huidige maand ${now.monthStr} verstuurd`);
}
// Previous month total — only within the first 7 days of the new month
if (+now.day <= 9) {
const prev = prevMonthStr(now.year, now.month);
if (global.get('zp-last-monthly-date') !== prev) {
const prevResult = cap['meter_power.total_earned_prevmonth']?.value;
if (prevResult != null) {
await mb.sendMonthly({ yearMonth: prev, batteryResult: String(prevResult) });
global.set('zp-last-monthly-date', prev);
log(`Daily: vorige maand ${prev} verstuurd`);
}
}
}
global.set('zp-last-daily-date', now.dateStr);
}
// ── Main ──────────────────────────────────────────────────────────────────────
const now = localNow();
log(`Tick: ${now.dateStr} ${now.hour}:xx lokale tijd`);
const battery = await getBattery();
assertSupportedPlugin(battery); // extra vangnet voor het geval er een oude id was hergebruikt
const cap = battery.capabilitiesObj;
const mb = new MijnbatterijApi();
try { await syncLive(cap, mb); } catch (e) { log(`Live mislukt, volgende tick: ${e.message}`); }
try { await syncDaily(cap, mb, now); } catch (e) { log(`Daily mislukt, volgende tick: ${e.message}`); }