ich habe weiter ein anderes:
const axios = require('axios');
// ======= OSTROM PROD =======
const CLIENT_ID = 'DEINE_ID';
const CLIENT_SECRET = 'DEIN_SECRET';
const ZIP = '59759';
// ======= INFLUXDB 2.x =======
const INFLUX_URL = 'http://192.168.178.103:8086';
const INFLUX_ORG = 'my_org';
const INFLUX_BUCKET = 'iobroker';
const INFLUX_TOKEN = 'DEIN_TOKEN';
// ======= IO BROKER =======
const BASE_DP = '0_userdata.0.ostrom';
const AUTH_URL = 'https://auth.production.ostrom-api.io/oauth2/token';
const API_URL = 'https://production.ostrom-api.io';
let isRunning = false;
createStates();
// API nur selten abrufen
schedule('30 14 * * *', fetchPrices);
// Status stündlich aktualisieren
schedule('0 * * * *', updateCurrentFromCache);
// beim Start nur Status aus Cache aktualisieren
setTimeout(updateCurrentFromCache, 10000);
// manueller API-Abruf
on({ id: `${BASE_DP}.control.runNow`, val: true }, async () => {
await setStateAsync(`${BASE_DP}.control.runNow`, false, true);
await fetchPrices();
});
async function fetchPrices() {
if (isRunning) return;
const lastFetch = getState(`${BASE_DP}.meta.lastFetch`)?.val || 0;
const now = Date.now();
if (now - lastFetch < 120000) {
log('Ostrom: Letzte API-Abfrage < 2 Minuten – überspringe', 'warn');
return;
}
isRunning = true;
try {
const token = await getAccessToken();
const prices = await getPrices(token);
const sorted = prices
.slice()
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
await setStateAsync(`${BASE_DP}.todayTomorrow.json`, JSON.stringify(sorted), true);
await updateCurrentFromPrices(sorted);
await updateForecast(sorted);
await writeToInflux(sorted);
await setStateAsync(`${BASE_DP}.meta.lastFetch`, now, true);
await setStateAsync(`${BASE_DP}.meta.lastFetchReadable`, new Date(now).toLocaleString('de-DE'), true);
log(`Ostrom OK: ${sorted.length} Preise`, 'info');
} catch (err) {
if (err.response?.status === 429) {
log('Ostrom 429: Rate Limit – später erneut versuchen', 'warn');
} else {
log(`Ostrom Fehler: ${err.message}`, 'error');
}
if (err.response) {
log(`Status: ${err.response.status}`, 'error');
log(JSON.stringify(err.response.data), 'error');
}
} finally {
isRunning = false;
}
}
async function updateCurrentFromCache() {
const json = getState(`${BASE_DP}.todayTomorrow.json`)?.val;
if (!json) {
log('Ostrom: Kein Preis-Cache vorhanden', 'warn');
return;
}
try {
const prices = JSON.parse(json);
await updateCurrentFromPrices(prices);
log('Ostrom current.isFree stündlich aktualisiert', 'info');
} catch (err) {
log(`Ostrom Cache Fehler: ${err.message}`, 'error');
}
}
async function updateCurrentFromPrices(prices) {
const current = findCurrentPrice(prices);
if (!current) return;
const grossKwhPrice = Number(current.grossKwhPrice ?? 0);
const grossKwhTaxAndLevies = Number(current.grossKwhTaxAndLevies ?? 0);
// echter variabler Bruttopreis ohne monatliche Kosten
const effectiveGrossKwhPrice = grossKwhPrice + grossKwhTaxAndLevies;
await setStateAsync(`${BASE_DP}.current.grossKwhPrice`, round(grossKwhPrice), true);
await setStateAsync(`${BASE_DP}.current.grossKwhTaxAndLevies`, round(grossKwhTaxAndLevies), true);
await setStateAsync(`${BASE_DP}.current.effectiveGrossKwhPrice`, round(effectiveGrossKwhPrice), true);
await setStateAsync(`${BASE_DP}.current.date`, current.date, true);
await setStateAsync(`${BASE_DP}.current.dateReadable`, new Date(current.date).toLocaleString('de-DE'), true);
await setStateAsync(`${BASE_DP}.current.hour`, new Date(current.date).getHours(), true);
await setStateAsync(`${BASE_DP}.current.isFree`, effectiveGrossKwhPrice <= 0, true);
}
async function updateForecast(prices) {
const effectivePrices = prices.map(p => {
const gross = Number(p.grossKwhPrice ?? 0);
const tax = Number(p.grossKwhTaxAndLevies ?? 0);
return {
date: p.date,
value: gross + tax
};
});
const values = effectivePrices.map(p => p.value);
const min = Math.min(...values);
const max = Math.max(...values);
const avg = values.reduce((a, b) => a + b, 0) / values.length;
const cheapest = effectivePrices.find(p => p.value === min);
const expensive = effectivePrices.find(p => p.value === max);
const nextFree = effectivePrices.find(p => new Date(p.date).getTime() >= Date.now() && p.value <= 0);
await setStateAsync(`${BASE_DP}.forecast.minEffectiveGrossKwhPrice`, round(min), true);
await setStateAsync(`${BASE_DP}.forecast.maxEffectiveGrossKwhPrice`, round(max), true);
await setStateAsync(`${BASE_DP}.forecast.avgEffectiveGrossKwhPrice`, round(avg), true);
await setStateAsync(`${BASE_DP}.forecast.cheapestDate`, cheapest?.date || '', true);
await setStateAsync(`${BASE_DP}.forecast.cheapestDateReadable`, cheapest ? new Date(cheapest.date).toLocaleString('de-DE') : '', true);
await setStateAsync(`${BASE_DP}.forecast.mostExpensiveDate`, expensive?.date || '', true);
await setStateAsync(`${BASE_DP}.forecast.mostExpensiveDateReadable`, expensive ? new Date(expensive.date).toLocaleString('de-DE') : '', true);
await setStateAsync(`${BASE_DP}.forecast.nextFreeDate`, nextFree?.date || '', true);
await setStateAsync(`${BASE_DP}.forecast.nextFreeDateReadable`, nextFree ? new Date(nextFree.date).toLocaleString('de-DE') : '', true);
await setStateAsync(`${BASE_DP}.forecast.hasFreeHour`, !!nextFree, true);
}
async function getAccessToken() {
const auth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
const res = await axios.post(
AUTH_URL,
'grant_type=client_credentials',
{
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: 15000
}
);
return res.data.access_token;
}
async function getPrices(token) {
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2, 0, 0, 0);
const res = await axios.get(`${API_URL}/spot-prices`, {
params: {
startDate: start.toISOString(),
endDate: end.toISOString(),
resolution: 'HOUR',
zip: ZIP
},
headers: {
Authorization: `Bearer ${token}`
},
timeout: 15000
});
return res.data.data || [];
}
async function writeToInflux(prices) {
if (!prices.length) return;
const lines = [];
for (const p of prices) {
const ts = BigInt(new Date(p.date).getTime()) * 1000000n;
const grossKwhPrice = Number(p.grossKwhPrice ?? 0);
const grossKwhTaxAndLevies = Number(p.grossKwhTaxAndLevies ?? 0);
const effectiveGrossKwhPrice = grossKwhPrice + grossKwhTaxAndLevies;
lines.push(
`ostrom_prices,source=production,zip=${escapeTag(ZIP)} ` +
`grossKwhPrice=${grossKwhPrice},` +
`grossKwhTaxAndLevies=${grossKwhTaxAndLevies},` +
`effectiveGrossKwhPrice=${effectiveGrossKwhPrice} ` +
`${ts.toString()}`
);
}
const url =
`${INFLUX_URL}/api/v2/write` +
`?org=${encodeURIComponent(INFLUX_ORG)}` +
`&bucket=${encodeURIComponent(INFLUX_BUCKET)}` +
`&precision=ns`;
await axios.post(url, lines.join('\n'), {
headers: {
Authorization: `Token ${INFLUX_TOKEN}`,
'Content-Type': 'text/plain'
},
timeout: 15000
});
}
function findCurrentPrice(prices) {
const now = new Date();
return prices.find(p => {
const start = new Date(p.date);
const end = new Date(start.getTime() + 3600000);
return now >= start && now < end;
});
}
async function createStates() {
await ensure(`${BASE_DP}.control.runNow`, 'boolean', true, true, 'button');
await ensure(`${BASE_DP}.meta.lastFetch`, 'number', 0, true, 'value');
await ensure(`${BASE_DP}.meta.lastFetchReadable`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.todayTomorrow.json`, 'string', '', true, 'json');
await ensure(`${BASE_DP}.current.grossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh');
await ensure(`${BASE_DP}.current.grossKwhTaxAndLevies`, 'number', 0, true, 'value', 'ct/kWh');
await ensure(`${BASE_DP}.current.effectiveGrossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh');
await ensure(`${BASE_DP}.current.date`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.current.dateReadable`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.current.hour`, 'number', 0, true, 'value', 'h');
await ensure(`${BASE_DP}.current.isFree`, 'boolean', false, true, 'indicator');
await ensure(`${BASE_DP}.forecast.minEffectiveGrossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh');
await ensure(`${BASE_DP}.forecast.maxEffectiveGrossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh');
await ensure(`${BASE_DP}.forecast.avgEffectiveGrossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh');
await ensure(`${BASE_DP}.forecast.cheapestDate`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.forecast.cheapestDateReadable`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.forecast.mostExpensiveDate`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.forecast.mostExpensiveDateReadable`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.forecast.nextFreeDate`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.forecast.nextFreeDateReadable`, 'string', '', true, 'text');
await ensure(`${BASE_DP}.forecast.hasFreeHour`, 'boolean', false, true, 'indicator');
}
async function ensure(id, type, def, write, role, unit = '') {
const exists = await existsStateAsync(id);
if (!exists) {
await createStateAsync(id, def, {
type,
role,
read: true,
write,
unit
});
}
}
function round(value) {
return Math.round(value * 1000) / 1000;
}
function escapeTag(value) {
return String(value)
.replace(/ /g, '\\ ')
.replace(/,/g, '\\,')
.replace(/=/g, '\\=');
}