iobroker.apg-info
Version:
1,070 lines (986 loc) • 60.5 kB
JavaScript
'use strict';
const utils = require('@iobroker/adapter-core');
const axios = require('axios');
const jsonExplorer = require('iobroker-jsonexplorer');
const stateAttr = require(`./lib/stateAttr.js`); // Load attribute library
const isOnline = require('@esm2cjs/is-online').default;
const { version } = require('./package.json');
const { getDataExaa1015, getDataExaa, getDataAwattar, getDataPeakHours, getDataEntsoe /*, getDataEpex*/ } = require('./lib/getData.js');
const { addDays, cleanDate, calcDate, pad, compareSecondColumn } = require('./lib/helpers.js');
// Constants
const MAX_DELAY = 25000; //25000
const API_TIMEOUT = 10000; //20000
class ApgInfo extends utils.Adapter {
/**
* @param {Partial<utils.AdapterOptions>} [options] Settings for the adapter instance
*/
constructor(options) {
super({
...options,
name: 'apg-info',
});
this.on('ready', this.onReady.bind(this));
// @ts-expect-error axiosInstance type
this.axiosInstance = axios.create({ timeout: API_TIMEOUT });
this.jsonExplorer = jsonExplorer;
//this.on('stateChange', this.onStateChange.bind(this));
//this.on('message', this.onMessage.bind(this));
this.on('unload', this.onUnload.bind(this));
jsonExplorer.init(this, stateAttr);
this.config_calculate = false;
this.feeAbsolute = 0;
this.feeRelative = 0;
this.vat = 0;
this.charges = 0;
this.gridCosts = 0;
this.token = '';
this.threshold = 10;
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
let country = '';
// Initialize adapter
jsonExplorer.sendVersionInfo(version);
this.log.info(`Started with JSON-Explorer version ${jsonExplorer.version}`);
if (this.config.threshold != undefined) {
this.threshold = this.config.threshold;
} else {
this.log.info('Market price threshold not found and set to 10');
}
this.config_forecast = this.config.forecast ?? false;
this.config_calculate = this.config.calculate ?? false;
this.config_peakHours = this.config.peakHours ?? false;
this.config_marketPrices = this.config.marketPrices ?? false;
this.config_quarterHourly = this.config.quarterHourly ?? false;
this.config_hourly = this.config.hourly ?? false;
this.config_details = this.config.details ?? false;
if (this.config_calculate == true) {
this.feeAbsolute = this.config.feeAbsolute ?? 0;
this.feeRelative = (this.config.feeRelative ?? 0) / 100;
this.vat = (this.config.vat ?? 0) / 100;
this.charges = (this.config.charges ?? 0) / 100;
this.gridCosts = this.config.gridCosts ?? 0;
}
if (this.config.country) {
country = this.config.country;
} else {
this.log.error('Country for market not found. Please confifure in Config');
this.terminate ? this.terminate(utils.EXIT_CODES.UNCAUGHT_EXCEPTION) : process.exit(0);
}
if (this.config.tokenEncrypted) {
this.token = this.config.tokenEncrypted;
} else {
const instanceId = `system.adapter.${this.name}.${this.instance}`;
const objInstance = await this.getForeignObjectAsync(instanceId);
if (objInstance?.native) {
let tokenUnEncrypted = objInstance.native.token;
if (tokenUnEncrypted) {
this.log.info(`Let's onetime encrypt the token...`);
objInstance.native.tokenEncrypted = this.encrypt(tokenUnEncrypted);
delete objInstance.native.token;
await this.setForeignObjectAsync(instanceId, objInstance);
this.token = tokenUnEncrypted;
this.log.info(`Token encrypted and saved in instance ${instanceId}`);
}
}
}
if (!this.token && country != 'at' && country != 'de') {
this.log.error('No token defined. Please check readme how to request!');
this.terminate ? this.terminate(utils.EXIT_CODES.UNCAUGHT_EXCEPTION) : process.exit(0);
}
if ((await isOnline()) == false) {
this.log.error('No internet connection detected');
this.terminate ? this.terminate(utils.EXIT_CODES.UNCAUGHT_EXCEPTION) : process.exit(0);
return;
}
this.log.debug('Internet connection detected. Everything fine!');
const callApiDelay = Math.floor(Math.random() * MAX_DELAY);
this.log.info(`Delay execution by ${callApiDelay}ms to better spread API calls`);
await jsonExplorer.sleep(callApiDelay);
await jsonExplorer.setLastStartTime();
const [resultPeakHours, resultMarketPrice] = await Promise.all([
this.executeRequestPeakHours(),
this.executeMarketPrice(country, this.config_forecast),
]);
if (resultPeakHours == 'error' || resultMarketPrice == 'error') {
this.terminate ? this.terminate(utils.EXIT_CODES.UNCAUGHT_EXCEPTION) : process.exit(0);
} else {
this.terminate ? this.terminate(0) : process.exit(0);
}
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
*
* @param {() => void} callback it is the callback that has to be called after all
*/
onUnload(callback) {
try {
this.log.info('cleaned everything up...');
this.unloaded = true;
callback();
} catch {
callback();
}
}
/**
* Handles json-object and creates states for market prices
*
* @param {string} country country of the market
* @param {boolean} forecast also checks 10.15 auction for next day
*/
async executeMarketPrice(country, forecast) {
if (this.config_marketPrices == false) {
await this.delObjectAsync('marketprice', { recursive: true });
await this.delObjectAsync('marketprice_quarter_hourly', { recursive: true });
return null;
}
this.log.debug('Execute market price retrieval');
let source1 = null,
source1q = null;
const configTraversJsonFalse = { replaceName: true, replaceID: true, level: 3, validateAttribute: false };
try {
const day0 = cleanDate(new Date());
const day1 = addDays(day0, 1);
let prices0 = [],
prices0q = [],
prices1 = [],
prices1q = [];
if (country == 'ch') {
const [pprices0, pprices1] = await Promise.all([
this._getAndProcessEntsoeData(false, country, false),
this._getAndProcessEntsoeData(true, country, false),
]);
prices0 = pprices0?.prices ?? [];
prices1 = pprices1?.prices ?? [];
} else {
({ prices0, prices1, source1, prices0q, prices1q, source1q } = await this._getAndProcessMarketData(country, forecast));
}
if (this.config_hourly) {
jsonExplorer.stateSetCreate('marketprice.today.date', 'date', day0.getTime());
jsonExplorer.stateSetCreate('marketprice.tomorrow.date', 'date', day1.getTime());
if (this.config_details) {
await jsonExplorer.traverseJson(prices0, 'marketprice.details.today', configTraversJsonFalse);
await jsonExplorer.traverseJson(prices1, 'marketprice.details.tomorrow', configTraversJsonFalse);
} else {
await this.delObjectAsync('marketprice.details', { recursive: true });
}
}
if (this.config_quarterHourly) {
jsonExplorer.stateSetCreate('marketprice_quarter_hourly.today.date', 'date', day0.getTime());
jsonExplorer.stateSetCreate('marketprice_quarter_hourly.tomorrow.date', 'date', day1.getTime());
if (this.config_details) {
await jsonExplorer.traverseJson(prices0q, 'marketprice_quarter_hourly.details.today', configTraversJsonFalse);
await jsonExplorer.traverseJson(prices1q, 'marketprice_quarter_hourly.details.tomorrow', configTraversJsonFalse);
} else {
await this.delObjectAsync('marketprice_quarter_hourly.details', { recursive: true });
}
}
const todayProcessed = this._processAndCategorizePrices(prices0, 'today', false);
const tomorrowProcessed = this._processAndCategorizePrices(prices1, 'tomorrow', false);
const todayProcessedq = this._processAndCategorizePrices(prices0q, 'today', true);
const tomorrowProcessedq = this._processAndCategorizePrices(prices1q, 'tomorrow', true);
if (!todayProcessed) {
return 'error';
}
const {
jDay: jDay0,
jDayBelowThreshold: jDay0BelowThreshold,
jDayAboveThreshold: jDay0AboveThreshold,
daysBelow: days0Below,
daysAbove: days0Above,
} = todayProcessed;
if (!tomorrowProcessed) {
return 'error';
}
const {
jDay: jDay1,
jDayBelowThreshold: jDay1BelowThreshold,
jDayAboveThreshold: jDay1AboveThreshold,
daysBelow: days1Below,
daysAbove: days1Above,
} = tomorrowProcessed;
if (!todayProcessedq) {
return 'error';
}
const {
jDay: jDay0q,
jDayBelowThreshold: jDay0BelowThresholdq,
jDayAboveThreshold: jDay0AboveThresholdq,
daysBelow: days0Belowq,
daysAbove: days0Aboveq,
} = todayProcessedq;
if (!tomorrowProcessedq) {
return 'error';
}
const {
jDay: jDay1q,
jDayBelowThreshold: jDay1BelowThresholdq,
jDayAboveThreshold: jDay1AboveThresholdq,
daysBelow: days1Belowq,
daysAbove: days1Aboveq,
} = tomorrowProcessedq;
//put data into an array
let arrBelow0 = Object.keys(jDay0BelowThreshold).map(key => [key, jDay0BelowThreshold[key]]);
let arrBelow1 = Object.keys(jDay1BelowThreshold).map(key => [key, jDay1BelowThreshold[key]]);
let arrAll0 = Object.keys(jDay0).map(key => [key, jDay0[key]]);
let arrAll1 = Object.keys(jDay1).map(key => [key, jDay1[key]]);
let arrBelow0q = Object.keys(jDay0BelowThresholdq).map(key => [key, jDay0BelowThresholdq[key]]);
let arrBelow1q = Object.keys(jDay1BelowThresholdq).map(key => [key, jDay1BelowThresholdq[key]]);
let arrAll0q = Object.keys(jDay0q).map(key => [key, jDay0q[key]]);
let arrAll1q = Object.keys(jDay1q).map(key => [key, jDay1q[key]]);
jDay0BelowThreshold.numberOfHours = days0Below;
jDay0AboveThreshold.numberOfHours = days0Above;
jDay1BelowThreshold.numberOfHours = days1Below;
jDay1AboveThreshold.numberOfHours = days1Above;
jDay0BelowThresholdq.numberOfSlots = days0Belowq;
jDay0AboveThresholdq.numberOfSlots = days0Aboveq;
jDay1BelowThresholdq.numberOfSlots = days1Belowq;
jDay1AboveThresholdq.numberOfSlots = days1Aboveq;
if (this.config_hourly) {
await jsonExplorer.traverseJson(jDay0, 'marketprice.today', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay0BelowThreshold, 'marketprice.belowThreshold.today', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay0AboveThreshold, 'marketprice.aboveThreshold.today', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay1, 'marketprice.tomorrow', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay1BelowThreshold, 'marketprice.belowThreshold.tomorrow', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay1AboveThreshold, 'marketprice.aboveThreshold.tomorrow', configTraversJsonFalse);
}
if (this.config_quarterHourly) {
await jsonExplorer.traverseJson(jDay0q, 'marketprice_quarter_hourly.today', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay1q, 'marketprice_quarter_hourly.tomorrow', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay0BelowThresholdq, 'marketprice_quarter_hourly.belowThreshold.today', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay0AboveThresholdq, 'marketprice_quarter_hourly.aboveThreshold.today', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay1BelowThresholdq, 'marketprice_quarter_hourly.belowThreshold.tomorrow', configTraversJsonFalse);
await jsonExplorer.traverseJson(jDay1AboveThresholdq, 'marketprice_quarter_hourly.aboveThreshold.tomorrow', configTraversJsonFalse);
}
//copy objets to use this for charts later
const arrAll0Copy = structuredClone(arrAll0);
const arrAll1Copy = structuredClone(arrAll1);
const arrAll0qCopy = structuredClone(arrAll0q);
const arrAll1qCopy = structuredClone(arrAll1q);
//now it is time to sort by prcie
arrBelow0.sort(compareSecondColumn);
arrBelow1.sort(compareSecondColumn);
arrAll0.sort(compareSecondColumn);
arrAll1.sort(compareSecondColumn);
arrBelow0q.sort(compareSecondColumn);
arrBelow1q.sort(compareSecondColumn);
arrAll0q.sort(compareSecondColumn);
arrAll1q.sort(compareSecondColumn);
//prepare sorted arrays to create states
let sortedHours0 = [],
sortedHours1 = [],
sortedHoursAll0 = [],
sortedHoursAll0q = [],
sortedHoursAll1 = [],
sortedHoursAll1q = [],
sortedHours0q = [],
sortedHours1q = [];
let sortedHours0Short = [],
sortedHours0Shortq = [],
sortedHours1Short = [],
sortedHours1Shortq = [],
sortedHours0ShortAll = [],
sortedHours0ShortAllq = [],
sortedHours1ShortAll = [],
sortedHours1ShortAllq = [];
let priceSum0 = 0,
priceSum0q = 0,
priceSum1 = 0,
priceSum1q = 0;
for (const idS in arrBelow0) {
sortedHours0[idS] = [arrBelow0[idS][0], arrBelow0[idS][1]];
sortedHours0Short[idS] = Number(arrBelow0[idS][0].substring(0, 2));
}
for (const idS in arrBelow0q) {
sortedHours0q[idS] = [arrBelow0q[idS][0], arrBelow0q[idS][1]];
sortedHours0Shortq[idS] = arrBelow0q[idS][0].substring(0, 5);
}
for (const idS in arrBelow1) {
sortedHours1[idS] = [arrBelow1[idS][0], arrBelow1[idS][1]];
sortedHours1Short[idS] = Number(arrBelow1[idS][0].substring(0, 2));
}
for (const idS in arrBelow1q) {
sortedHours1q[idS] = [arrBelow1q[idS][0], arrBelow1q[idS][1]];
sortedHours1Shortq[idS] = arrBelow1q[idS][0].substring(0, 5);
}
for (const idS in arrAll0) {
sortedHoursAll0[idS] = [arrAll0[idS][0], arrAll0[idS][1]];
sortedHours0ShortAll[idS] = Number(arrAll0[idS][0].substring(0, 2));
priceSum0 = priceSum0 + arrAll0[idS][1];
}
for (const idS in arrAll0q) {
sortedHoursAll0q[idS] = [arrAll0q[idS][0], arrAll0q[idS][1]];
sortedHours0ShortAllq[idS] = arrAll0q[idS][0].substring(0, 5);
priceSum0q = priceSum0q + arrAll0q[idS][1];
}
for (const idS in arrAll1) {
sortedHoursAll1[idS] = [arrAll1[idS][0], arrAll1[idS][1]];
sortedHours1ShortAll[idS] = Number(arrAll1[idS][0].substring(0, 2));
priceSum1 = priceSum1 + arrAll1[idS][1];
}
for (const idS in arrAll1q) {
sortedHoursAll1q[idS] = [arrAll1q[idS][0], arrAll1q[idS][1]];
sortedHours1ShortAllq[idS] = arrAll1q[idS][0].substring(0, 5);
priceSum1q = priceSum1q + arrAll1q[idS][1];
}
let price0Avg, price1Avg, price0Avgq, price1Avgq;
if (priceSum0 == 0) {
price0Avg = null;
} else {
price0Avg = Math.round((priceSum0 / 24) * 1000) / 1000;
}
if (priceSum1 == 0) {
price1Avg = null;
} else {
price1Avg = Math.round((priceSum1 / 24) * 1000) / 1000;
}
if (priceSum0q == 0) {
price0Avgq = null;
} else {
price0Avgq = Math.round((priceSum0q / (24 * 4)) * 1000) / 1000;
}
if (priceSum1q == 0) {
price1Avgq = null;
} else {
price1Avgq = Math.round((priceSum1q / (24 * 4)) * 1000) / 1000;
}
if (this.config_hourly) {
await jsonExplorer.traverseJson(sortedHours0, 'marketprice.belowThreshold.today_sorted', configTraversJsonFalse);
await jsonExplorer.traverseJson(sortedHours1, 'marketprice.belowThreshold.tomorrow_sorted', configTraversJsonFalse);
await jsonExplorer.traverseJson(sortedHoursAll0, 'marketprice.today_sorted', configTraversJsonFalse);
await jsonExplorer.traverseJson(sortedHoursAll1, 'marketprice.tomorrow_sorted', configTraversJsonFalse);
await jsonExplorer.stateSetCreate(
'marketprice.belowThreshold.today_sorted.short',
'today sorted short',
JSON.stringify(sortedHours0Short),
false,
);
await jsonExplorer.stateSetCreate(
'marketprice.belowThreshold.tomorrow_sorted.short',
'tomorrow sorted short',
JSON.stringify(sortedHours1Short),
false,
);
await jsonExplorer.stateSetCreate(
'marketprice.today_sorted.short',
'today sorted short',
JSON.stringify(sortedHours0ShortAll),
false,
);
await jsonExplorer.stateSetCreate(
'marketprice.tomorrow_sorted.short',
'tomorrow sorted short',
JSON.stringify(sortedHours1ShortAll),
false,
);
await jsonExplorer.stateSetCreate('marketprice.today.average', 'average', price0Avg, false);
await jsonExplorer.stateSetCreate('marketprice.tomorrow.average', 'average', price1Avg, false);
}
if (this.config_quarterHourly) {
await jsonExplorer.traverseJson(
sortedHours0q,
'marketprice_quarter_hourly.belowThreshold.today_sorted',
configTraversJsonFalse,
false,
);
await jsonExplorer.traverseJson(
sortedHours1q,
'marketprice_quarter_hourly.belowThreshold.tomorrow_sorted',
configTraversJsonFalse,
false,
);
await jsonExplorer.traverseJson(sortedHoursAll0q, 'marketprice_quarter_hourly.today_sorted', configTraversJsonFalse, false);
await jsonExplorer.traverseJson(sortedHoursAll1q, 'marketprice_quarter_hourly.tomorrow_sorted', configTraversJsonFalse, false);
await jsonExplorer.stateSetCreate(
'marketprice_quarter_hourly.today_sorted.short',
'today sorted short',
JSON.stringify(sortedHours0ShortAllq),
false,
);
await jsonExplorer.stateSetCreate(
'marketprice_quarter_hourly.tomorrow_sorted.short',
'tomoorrow sorted short',
JSON.stringify(sortedHours1ShortAllq),
false,
);
await jsonExplorer.stateSetCreate(
'marketprice_quarter_hourly.belowThreshold.today_sorted.short',
'today sorted short',
JSON.stringify(sortedHours0Shortq),
false,
);
await jsonExplorer.stateSetCreate(
'marketprice_quarter_hourly.belowThreshold.tomorrow_sorted.short',
'tomorrow sorted short',
JSON.stringify(sortedHours1Shortq),
false,
);
await jsonExplorer.stateSetCreate('marketprice_quarter_hourly.today.average', 'average', price0Avgq, false);
await jsonExplorer.stateSetCreate('marketprice_quarter_hourly.tomorrow.average', 'average', price1Avgq, false);
}
if (this.config_hourly) {
await this.createCharts(arrAll0Copy, arrAll1Copy, source1, false);
await jsonExplorer.checkExpire('marketprice.*');
await jsonExplorer.deleteObjectsWithNull('marketprice.*Threshold.*');
await jsonExplorer.deleteObjectsWithNull('marketprice.details.*');
}
if (this.config_quarterHourly) {
await this.createCharts(arrAll0qCopy, arrAll1qCopy, source1q, true);
await jsonExplorer.checkExpire('marketprice_quarter_hourly.*');
await jsonExplorer.deleteObjectsWithNull('marketprice_quarter_hourly.*Threshold.*');
await jsonExplorer.deleteObjectsWithNull('marketprice_quarter_hourly.details.*');
}
if (!this.config_quarterHourly) {
await this.delObjectAsync('marketprice_quarter_hourly', { recursive: true });
}
if (!this.config_hourly) {
await this.delObjectAsync('marketprice', { recursive: true });
}
} catch (error) {
let eMsg = `Error in executeMarketPrice(): ${error}`;
this.log.error(eMsg);
console.error(eMsg);
this.sendSentry(error);
}
}
/**
* Processes and categorizes market prices for a given day.
*
* @param {any[] | null} prices - The array of price objects.
* @param {string} dayString - A string identifier for the day (e.g., 'today', 'tomorrow').
* @param {boolean} quaterly - Indicates if the prices are in quarterly format.
* @returns {{jDay: object, jDayBelowThreshold: object, jDayAboveThreshold: object, daysBelow: number, daysAbove: number} | null} return
*/
_processAndCategorizePrices(prices, dayString, quaterly = false) {
const jDay = {};
const jDayBelowThreshold = {};
const jDayAboveThreshold = {};
let daysBelow = 0;
let daysAbove = 0;
for (const idS in prices) {
if (prices[idS].Price == undefined) {
this.log.error(`No marketprice found in marketprice-result for ${dayString}!`);
return null;
}
const product = prices[idS].Product;
const marketprice = this.calcPrice(prices[idS].Price / 10);
this.log.debug(`Marketprice for product ${product} is ${marketprice}`);
let range;
if (quaterly) {
const productText = prices[idS].ProductText;
const regexZeit = /(\d{2}:\d{2}\s*-\s*\d{2}:\d{2})/;
const matchZeit = productText.match(regexZeit);
if (matchZeit && matchZeit.length > 1) {
range = matchZeit[1].replace(/ /g, '');
}
} else {
const sEndHour = product.substring(1, 3);
const iEndHour = Number(sEndHour);
const iBeginHour = iEndHour - 1;
const sBeginHour = pad(iBeginHour, 2);
range = `${sBeginHour}_to_${sEndHour}`;
}
jDay[range] = marketprice;
if (marketprice < this.threshold) {
jDayBelowThreshold[range] = marketprice;
daysBelow++;
} else {
jDayAboveThreshold[range] = marketprice;
daysAbove++;
}
}
this.log.debug(`Day prices for ${dayString} look like ${JSON.stringify(jDay)}`);
return { jDay, jDayBelowThreshold, jDayAboveThreshold, daysBelow, daysAbove };
}
/**
* Fetches and processes market data from Awattar/Exaa for today and tomorrow.
*
* @param {string} country The country code for the API request.
* @param {boolean} forecast also checks 10.15 auction for next day
* @returns {Promise<{prices0: any[], prices1: any[], source1: string |null, prices0q: any, prices1q: any, source1q: string |null}>} An object containing the processed prices for today and tomorrow and the source for tomorrow.
*/
async _getAndProcessMarketData(country, forecast) {
//nst todayDate = cleanDate(new Date());
//const tomorrowDate = addDays(todayDate, 1);
let prices0Entsoe = null,
prices1Entsoe = null;
let prices0Awattar, prices1Awattar, prices0Exaa, prices1Exaa, prices1Exaa1015, prices0Epex, prices1Epex;
let todayResult, tomorrowResult, todayResultq, tomorrowResultq;
const useEntsoe = this.token == null || this.token.length < 10 ? false : true;
const [eXaaToday, eXaaTomorrow] = await Promise.all([getDataExaa(this, false, country), getDataExaa(this, true, country)]);
//check for provider for today for quarter-hourly
if (this.config_quarterHourly) {
this.log.info(`Let's check for quarter-hourly market data`);
prices0Exaa = eXaaToday?.q ?? null;
if (prices0Exaa == null) {
this.log.info(`No quarter-hourly market data from Exaa for today, let's try Entsoe`);
if (useEntsoe) {
prices0Entsoe = await this._getAndProcessEntsoeData(false, country, false);
} else {
this.log.info(`No token defined for Entsoe, skipped!`);
}
/*if (useEntsoe && prices0Entsoe?.prices == null) {
this.log.info(`No quarter-hourly market data from Entsoe for today, let's try Epex`);
prices0Epex = await getDataEpex(this, false, country);
if (prices0Epex?.data?.[0] && new Date(prices0Epex.meta.deliveryDate).getTime() === todayDate.getTime()) {
this.log.info('Todays quarter-hourly market data from Epex available');
this.log.debug(`Todays quarter-hourly market data result from Epex is: ${JSON.stringify(prices0Epex)}`);
prices0Epex = prices0Epex.data;
} else {
prices0Epex = null;
this.log.error('No quarter-hourly market data for today!');
}
}*/
}
//Tomorrow
prices1Exaa = eXaaTomorrow?.q ?? null;
if (prices1Exaa == null) {
this.log.info(`No quarter-hourly market data from Exaa for tomorrow, let's try Entsoe`);
if (useEntsoe) {
prices1Entsoe = await this._getAndProcessEntsoeData(true, country, forecast);
} else {
this.log.info(`No token defined for Entsoe, skipped!`);
}
/*if (useEntsoe && prices1Entsoe?.prices == null) {
this.log.info(`No quarter-hourly market data from Entsoe for tomorrow, let's try Epex`);
prices1Epex = await getDataEpex(this, true, country);
if (prices1Epex?.data?.[0] && new Date(prices1Epex.meta.deliveryDate).getTime() === tomorrowDate.getTime()) {
this.log.info('Tomorrows quarter-hourly market data from Epex available');
this.log.debug(`Tomorrows quarter-hourly market data result from Epex is: ${JSON.stringify(prices0Epex)}`);
prices1Epex = prices1Epex.data;
} else {
prices1Epex = null;
this.log.info('No quarter-hourly market data for tomorrow!');
}
}*/
}
if (prices0Exaa == null && prices0Entsoe?.prices == null) {
this.log.error('No quarter-hourly market data for today!');
}
if (prices0Entsoe?.prices != null) {
this.log.info('Found Entsoe quarter-hourly market data for today!');
}
if (prices0Exaa != null) {
this.log.info('Found Epex quarter-hourly market data for today!');
}
if (prices1Exaa == null && prices1Entsoe?.prices == null) {
this.log.info('No quarter-hourly market data for tomorrow!');
}
if (prices1Entsoe?.prices != null) {
this.log.info('Found Entsoe quarter-hourly market data for tomorrow!');
}
if (prices1Exaa != null) {
this.log.info('Found Epex quarter-hourly market data for tomorrow!');
}
todayResultq =
prices0Entsoe != null ? prices0Entsoe : this._processMarketPrices('today', prices0Awattar, prices0Exaa, null, prices0Epex, true);
tomorrowResultq =
prices1Entsoe != null
? prices1Entsoe
: this._processMarketPrices('tomorrow', prices1Awattar, prices1Exaa, prices1Exaa1015, prices1Epex, true);
}
if (this.config_hourly) {
this.log.info(`Let's check for hourly market data`);
//check for provider for today for hourly
prices0Exaa = eXaaToday?.h ?? null;
if (prices0Exaa == null) {
this.log.info(`No hourly market data from Exaa for today, let's try Awattar`);
prices0Awattar = await getDataAwattar(this, false, country);
if (prices0Awattar?.data?.[0]) {
this.log.info('Todays hourly market data from Awattar available');
this.log.debug(`Todays hourly market data result from Awattar is: ${JSON.stringify(prices0Awattar)}`);
} else {
this.log.error('No hourly market data for today!');
}
} else {
this.log.debug(`Todays hourly market data result from Exaa is: ${JSON.stringify(prices0Exaa)}`);
}
//check for provider for tomorrow
prices1Exaa = eXaaTomorrow?.h ?? null;
if (prices1Exaa == null) {
this.log.info(`No hourly market data from Exaa for tomorrow, let's try Awattar`);
prices1Awattar = await getDataAwattar(this, true, country);
if (prices1Awattar?.data?.[0]) {
this.log.info('Tomorrows hourly market data from Awattar available');
this.log.debug(`Tomorrow hourly market data result from Awattar is: ${JSON.stringify(prices1Awattar)}`);
} else {
if (forecast) {
this.log.info('No hourly market data from Awattar for tomorrow , last chance Exaa 10.15 auction!');
const eXaa1015 = await getDataExaa1015(this, country);
prices1Exaa1015 = eXaa1015;
if (prices1Exaa1015) {
this.log.info('Market hourly data from Exaa 10.15 auction available');
} else {
this.log.info('Bad luck for Exaa 10.15 auction');
}
this.log.debug(`Tomorrows hourly market data result from Exaa 10.15 auction is: ${JSON.stringify(prices1Exaa1015)}`);
} else {
this.log.info('No hourly market data from Awattar for tomorrow');
}
}
} else {
this.log.debug(`Tomorrows hourly market data result from Exaa is: ${JSON.stringify(prices1Exaa)}`);
}
todayResult = this._processMarketPrices('today', prices0Awattar, prices0Exaa, null, prices0Epex, false);
tomorrowResult = this._processMarketPrices('tomorrow', prices1Awattar, prices1Exaa, prices1Exaa1015, prices1Epex, false);
}
return {
prices0: todayResult?.prices ?? [],
prices1: tomorrowResult?.prices ?? [],
source1: tomorrowResult?.source ?? null,
prices0q: todayResultq?.prices ?? [],
prices1q: tomorrowResultq?.prices ?? [],
source1q: tomorrowResultq?.source ?? null,
};
}
/**
* Fetches and processes market data from Entsoe for today and tomorrow.
* Includes a retry mechanism for network-related errors.
*
* @param {boolean} tomorrow if true, calculation is for tomorrow
* @param {string} country The country code for the API request.
* @param {boolean} forecast if true 1015 forecast is checked
* @returns {Promise<{prices: any[] | null, source: string}>} prices An object containing the processed prices and the source.
*/
async _getAndProcessEntsoeData(tomorrow, country, forecast) {
const day = tomorrow ? 'tomorrow' : 'today';
let pricesEntsoe, prices;
let source = '';
if (!tomorrow) {
pricesEntsoe = await getDataEntsoe(this, false, country);
this.log.debug(`pricesEntsoe for ${day}: ${JSON.stringify(pricesEntsoe)}`);
prices = this._processEntsoeData(pricesEntsoe, 'today', false) || [];
this.log.debug(`Prices w/o forecast for ${day}: ${JSON.stringify(prices)}`);
source = 'entsoe';
if (prices.length > 50) {
jsonExplorer.stateSetCreate('marketprice_quarter_hourly.today.source', 'Source', source);
} else if (prices.length > 0) {
jsonExplorer.stateSetCreate('marketprice.today.source', 'Source', source);
} else {
source = '';
}
} else {
pricesEntsoe = await getDataEntsoe(this, true, country);
this.log.debug(`pricesEntsoe for ${day}: ${JSON.stringify(pricesEntsoe)}`);
prices = this._processEntsoeData(pricesEntsoe, 'tomorrow', false) || [];
this.log.debug(`Prices w/o forecast for ${day}: ${JSON.stringify(prices)}`);
source = 'entsoe';
if (prices.length == 0 && forecast) {
prices = this._processEntsoeData(pricesEntsoe, 'tomorrow', true) || [];
this.log.debug(`Prices with forecast for ${day}: ${JSON.stringify(prices)}`);
source = 'entsoe1015';
if (prices.length > 0) {
this.log.info('Data from Entsoe for 10:15 auction available');
}
}
if (prices.length > 50) {
jsonExplorer.stateSetCreate('marketprice_quarter_hourly.tomorrow.source', 'Source', source);
} else if (prices.length > 0) {
jsonExplorer.stateSetCreate('marketprice.tomorrow.source', 'Source', source);
} else {
source = '';
}
}
prices = prices.length == 0 ? null : prices;
return { prices, source };
}
/**
* Processes market prices from different sources for a given day.
* It selects the best available data source and converts it to a unified format.
*
* @param {'today' | 'tomorrow'} day - The day to process ('today' or 'tomorrow').
* @param {any} awattarData - Data from Awattar API.
* @param {any} exaaData - Data from EXAA Market Coupling API.
* @param {any} exaa1015Data - Optional data from EXAA 10:15 auction API (for tomorrow).
* @param {any} epexData - Data from Epex Market
* @param {boolean} quarter - quater hourly data yes/no
* @returns {{prices: any[], source: string}} The processed prices and the source name.
*/
_processMarketPrices(day, awattarData, exaaData, exaa1015Data, epexData, quarter) {
let prices = [];
let source = '';
if (exaaData) {
prices = this._convertExaaData(exaaData);
source = 'exaaMC';
} else if (day === 'tomorrow' && exaa1015Data) {
prices = this._convertExaa1015Data(exaa1015Data);
source = 'exaa1015';
} else if (awattarData?.data?.[0]) {
prices = this._convertAwattarData(awattarData);
source = 'awattar';
} else if (epexData) {
prices = this._convertEpexData(epexData);
source = 'epex';
}
if (source) {
if (quarter) {
jsonExplorer.stateSetCreate(`marketprice_quarter_hourly.${day}.source`, 'Source', source);
} else {
jsonExplorer.stateSetCreate(`marketprice.${day}.source`, 'Source', source);
}
}
return { prices, source };
}
/**
* Converts data from the Awattar API to the internal price format.
*
* @param {any} awattarData - The raw data from Awattar.
* @returns {any[]} The converted price data.
*/
_convertAwattarData(awattarData) {
const prices = [];
for (const idS in awattarData.data) {
prices[idS] = {};
prices[idS].Price = awattarData.data[idS].marketprice;
const start = new Date(awattarData.data[idS].start_timestamp);
const iHour = start.getHours() + 1;
const sHour = pad(iHour, 2);
prices[idS].Product = `H${sHour}`;
}
return prices;
}
/**
* Converts data from the EXAA 10:15 auction API to the internal price format.
*
* @param {any} exaa1015Data - The raw data from EXAA 10:15 auction.
* @returns {any[]} The converted price data.
*/
_convertExaa1015Data(exaa1015Data) {
const prices = [];
for (const idS in exaa1015Data) {
prices[idS] = {};
prices[idS].Price = exaa1015Data[idS].y;
const iHour = exaa1015Data[idS].x;
const sHour = pad(iHour, 2);
prices[idS].Product = `H${sHour}`;
}
this.log.debug(`prices1Exaa1015 converted to: ${JSON.stringify(prices)}`);
return prices;
}
/**
* Converts data from the EXAA 10:15 auction API to the internal price format.
*
* @param {any} epexData - The raw data from EXAA 10:15 auction.
* @returns {any[]} The converted price data.
*/
_convertEpexData(epexData) {
let data = epexData ?? null;
const prices = [];
for (const idS in data) {
prices[idS] = {};
const index = data[idS]['index'];
const ersteStelle = Math.ceil(index / 4);
const zweiteStelle = index % 4 === 0 ? 4 : index % 4;
const prod = `Q${pad(ersteStelle, 2)}_${zweiteStelle}`;
prices[idS].Price = data[idS]['Price(€/MWh)'];
prices[idS].Product = prod;
prices[idS].ProductText = `${prod} (${data[idS].start}-${data[idS].end})`;
//prices[idS].SellVolume = data[idS]['Sell Volume(MWh)'];
//prices[idS].TotalVol = data[idS]['Volume(MWh)'];
prices[idS].id = `${data[idS].start}-${data[idS].end}`;
}
this.log.debug(`pricesEpex converted to: ${JSON.stringify(prices)}`);
return prices;
}
/**
* Converts data from the EXAA API to the internal price format.
*
* @param {any} exaaData - The raw data from EXAA auction.
* @returns {any[]} The converted price data.
*/
_convertExaaData(exaaData) {
//only for quarter-hourly data we have to extract the id from ProductText
if (exaaData?.[0] != null && exaaData[0].ProductText?.substring(0, 1) == 'q') {
for (const item of exaaData) {
const productText = item.ProductText;
const regexZeit = /(\d{2}:\d{2}\s*-\s*\d{2}:\d{2})/;
const matchZeit = productText.match(regexZeit);
if (matchZeit && matchZeit.length > 1) {
item.id = matchZeit[1].replace(/ /g, '');
}
}
exaaData = exaaData.filter(item => item.id != null);
}
if (exaaData?.[0] != null) {
for (const item of exaaData) {
delete item.SellVolume;
delete item.TotalVol;
delete item.BuyVolume;
delete item.AuctionDay;
}
}
this.log.debug(`convertExaaData result is: ${JSON.stringify(exaaData)}`);
return exaaData;
}
/**
* Finds all TimeSeries objects where the
* 'classificationSequence_AttributeInstanceComponent.position' is "1".
*
* @param {object} data - The fully parsed JSON data object containing the TimeSeries array.
* @param {number} filter - 1 for MC and 2 for 10:15
* @returns {Array<object>} An array of matching TimeSeries objects.
*/
filterTimeSeriesByPosition(data, filter) {
// 1. Get the TimeSeries array safely.
let allTimeSeries = data?.TimeSeries ?? null;
if (allTimeSeries == null) {
return [];
}
//if there is no array convert into array
if (!Array.isArray(allTimeSeries)) {
const domain = String(allTimeSeries?.['in_Domain.mRID']?._text ?? '');
this.log.debug(`Domain is ${domain}`);
if (domain.includes('CH-SWISS')) {
return [allTimeSeries];
}
allTimeSeries = [allTimeSeries];
}
// 2. Filter the array based on the position criteria.
const matchingSeries = allTimeSeries.filter(ts => {
try {
// Access the nested property using bracket notation because of the dots in the key.
const position = ts['classificationSequence_AttributeInstanceComponent.position']?._text;
// Check if the extracted text value is exactly '1'.
return position === String(filter);
} catch (error) {
// Log an error if a specific TimeSeries entry is malformed and skip it.
// @ts-expect-error error ok
console.warn(`Skipping TimeSeries entry due to error: ${error.message}`);
return false;
}
});
return matchingSeries;
}
/**
* Processes the raw data from the Entsoe API.
*
* @param {any} entsoeData The raw data object from the Entsoe API.
* @param {string} dayString A string like 'today' or 'tomorrow' for logging purposes.
* @param {boolean} earlyAuction if true, 10:15 auction will be used
* @returns {Array<any> | null} An array with the processed price data or null if processing fails.
*/
_processEntsoeData(entsoeData, dayString, earlyAuction = false) {
const filter = earlyAuction ? 2 : 1;
if (!entsoeData) {
this.log.debug(`No Entsoe data provided for ${dayString}.`);
return null;
}
this.log.debug(`Entsoe data for ${dayString}: ${JSON.stringify(entsoeData)}`);
console.log(`Entsoe data for ${dayString}: ${JSON.stringify(entsoeData)}`);
if (entsoeData.TimeSeries[0] == null && entsoeData.TimeSeries == null) {
this.log.error(`No data available for ${dayString}!`);
return null;
}
let timeSeries = this.filterTimeSeriesByPosition(entsoeData, filter)[0];
let point = timeSeries?.Period[0]?.Point ?? null;
if (point == null) {
point = timeSeries?.Period?.Point;
}
point = point == null ? null : this.fillMissingPositions(point);
let prices = [];
const length = point ? point.length : 0;
for (let i = 0; i < length; i++) {
const ii = String(i);
prices[ii] = {};
const price = parseFloat(point[i].price_amount._text);
const sPosition = pad(point[i].position._text, 2);
//quater-hourly
if (length > 50) {
const slot = parseInt(point[i].position._text) - 1;
const nextSlot = slot + 1;
const hour = pad(Math.floor(slot / 4), 2);
const minute = pad((slot % 4) * 15, 2);
const nextHour = pad(Math.floor(nextSlot / 4), 2);
const nextMinute = pad((nextSlot % 4) * 15, 2);
prices[ii].Product = `Q${sPosition}`;
prices[ii].ProductText = `Q${sPosition} (${hour}:${minute}-${nextHour}:${nextMinute})`;
prices[ii].id = `${hour}:${minute}-${nextHour}:${nextMinute}`;
} else {
prices[ii].Product = `H${sPosition}`;
}
prices[ii].Price = price;
}
return prices;
}
/**
* Fills in missing entries in an array of position/price objects.
* Missing entries copy the price_amount from the previous existing entry.
*
* @param {Array<object>} dataArray - The input array with potentially missing positions.
* @returns {Array<object>} - The new array with all positions filled sequentially.
*/
fillMissingPositions(dataArray) {
// 1. Convert price_amount and position to numbers for easier processing and sorting.
// This step also prepares the data structure for the final result.
const processedData = dataArray.map(item => ({
position: parseInt(item.position._text),
priceAmount: parseFloat(item.price_amount._text),
}));
// 2. Sort the array by position to ensure correct processing
processedData.sort((a, b) => a.position - b.position);
const filledArray = [];
let lastPriceAmount = null;
// Determine the start and end of the sequence
const startPosition = processedData.length > 0 ? processedData[0].position : 1;
const endPosition = processedData.length > 0 ? processedData[processedData.length - 1].position : 0;
// Create a map for quick look-up of existing positions
const dataMap = new Map(processedData.map(item => [item.position, item.priceAmount]));
// 3. Iterate from the first found position to the last found position
for (let currentPosition = startPosition; currentPosition <= endPosition; currentPosition++) {
if (dataMap.has(currentPosition)) {
// Found existing position
const currentPrice = dataMap.get(currentPosition);
filledArray.push({
position: { _text: String(currentPosition) },
price_amount: { _text: String(currentPrice) },
});
// Update the last known price for subsequent missing entries
lastPriceAmount = currentPrice;
} else if (lastPriceAmount !== null) {
// Position is missing and we have a previous price, so we fill it in
// The price is copied from 'lastPriceAmount'
filledArray.push({
position: { _text: String(currentPosition) },
price_amount: { _text: String(lastPriceAmount) },
});
} else {
// Case: Missing position at the very beginning of the data (unlikely but safe to handle)
// You might want to handle this differently, but for now, we skip it.
console.warn(`Missing position ${currentPosition} found before any price could be established.`);
}
}
return filledArray;
}
/**
* Handles json-object and creates states for peak hours
*/
async executeRequestPeakHours() {
if (this.config_peakHours == false) {
await this.delObjectAsync('peakTime', { recursive: true });
return null;
}
try {
let result = await getDataPeakHours(this);
this.log.debug(`Peak hour result is: ${JSON.stringify(result)}`);
if (!result || !result.StatusInfos) {
this.log.error('No data available for peak-result!');
return;
}
let day0 = cleanDate(new Date());
let day1 = addDays(day0, 1);
let day2 = addDays(day0, 2);
let day3 = addDays(day0, 3);
let day4 = addDays(day0, 4);
let jDay0 = {},
jDay1 = {},
jDay2 = {},
jDay3 = {},
jDay4 = {},
jDayAll = {};
let iHour = 0;
let sHour = '';
let i = 1;
for (const idS in result.StatusInfos) {
if