UNPKG

iobroker.apg-info

Version:
985 lines (890 loc) 47.4 kB
'use strict'; /* * Created with @iobroker/create-adapter v1.25.0 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); // Load your modules here, e.g.: const axios = require('axios'); const convert = require('xml-js'); const jsonExplorer = require('iobroker-jsonexplorer'); const stateAttr = require(`${__dirname}/lib/stateAttr.js`); // Load attribute library const isOnline = require('@esm2cjs/is-online').default; const { version } = require('./package.json'); //global variables let threshold = 10; const maxDelay = 25000; //25000 // @ts-ignore const axiosInstance = axios.create({ timeout: 30000 }); //30000 class ApgInfo extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options={}] */ constructor(options) { super({ ...options, name: 'apg-info', }); this.on('ready', this.onReady.bind(this)); //this.on('objectChange', this.onObjectChange.bind(this)); //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.calculate = false; this.feeAbsolute = 0; this.feeRelative = 0; this.vat = 0; this.charges = 0; this.gridCosts = 0; this.token = ''; } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { let country = ''; let forecast = false; // Initialize adapter jsonExplorer.sendVersionInfo(version); this.log.info('Started with JSON-Explorer version ' + jsonExplorer.version); if (this.config.threshold) threshold = this.config.threshold; else this.log.info('Market price threshold not found and set to 10'); if (this.config.forecast != undefined) forecast = this.config.forecast; else this.log.info('Forecast config not found and set to disbaled'); if (this.config.calculate != undefined) this.calculate = this.config.calculate; if (this.calculate == true) { if (this.config.feeAbsolute != undefined) this.feeAbsolute = this.config.feeAbsolute; if (this.config.feeRelative != undefined) this.feeRelative = this.config.feeRelative / 100; if (this.config.vat != undefined) this.vat = this.config.vat / 100; if (this.config.charges != undefined) this.charges = this.config.charges / 100; if (this.config.gridCosts != undefined) this.gridCosts = this.config.gridCosts; } 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 (country != 'at' && country != 'de') { 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) { 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; } else { this.log.debug('Internet connection detected. Everything fine!'); } const callApiDelay = Math.floor(Math.random() * maxDelay); this.log.info(`Delay execution by ${callApiDelay}ms to better spread API calls`); await jsonExplorer.sleep(callApiDelay); await jsonExplorer.setLastStartTime(); let resultPeakHours = await this.ExecuteRequestPeakHours(); let resultDayAhead = await this.ExecuteRequestDayAhead(country, forecast); if (resultPeakHours == 'error' || resultDayAhead == '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 */ onUnload(callback) { try { this.log.info('cleaned everything up...'); this.unloaded = true; callback(); } catch { callback(); } } /* /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ /* onStateChange(id, state) { if (state) { // The state was changed this.log.debug(`state ${id} changed: ${state.val} (ack = ${state.ack})`); } else { // The state was deleted this.log.debug(`state ${id} deleted`); } }*/ /** * Retrieves peak hours from REST-API */ async getDataPeakHours() { let uri = `https://awareness.cloud.apg.at/api/v1/PeakHourStatus`; this.log.debug(`API-Call ${uri}`); console.log(`API-Call ${uri}`); return new Promise((resolve, reject) => { axiosInstance.get(uri) .then((response) => { if (!response || !response.data) { throw new Error(`getDataPeakHours(): Respone empty for URL ${uri} with status code ${response.status}`); } else { this.log.debug(`Response in getDataPeakHours(): [${response.status}] ${JSON.stringify(response.data)}`); console.log(`Response in getDataPeakHours(): [${response.status}] ${JSON.stringify(response.data)}`); resolve(response.data); } }) .catch(error => { if (error?.response?.data) { console.error('Error in getDataPeakHours(): ' + error + ' with response ' + JSON.stringify(error.response.data)); this.log.error('Error to get peak hours ' + error + ' with response ' + JSON.stringify(error.response.data)); } else { console.error('Error in getDataPeakHours(): ' + error); this.log.error('Error to get peak hours ' + error); } if (error?.response?.status >= 500) resolve(null); else reject(error); }); }); } /** * Retrieves marketdata from REST-API from Exaa * @param {boolean} tomorrow * @param {string} country country of the market */ async getDataDayAheadExaa(tomorrow, country) { const day0 = cleanDate(new Date()); const day1 = addDays(day0, 1); let day = new Date(); if (tomorrow) day = day1; else day = day0; const dateStringToday = `${day.getFullYear()}-${day.getMonth() + 1}-${day.getDate()}`; const uri = `https://www.exaa.at/data/trading-results?delivery_day=${dateStringToday}&market=${country}&auction=market_coupling`; this.log.debug(`API-Call ${uri}`); console.log(`API-Call ${uri}`); return new Promise((resolve, reject) => { axiosInstance.get(uri) .then((response) => { if (!response || !response.data) { throw new Error(`getDataDayAheadExaa(): Respone empty for URL ${uri} with status code ${response.status}`); } else { this.log.debug(`Response in getDataDayAheadExaa(): [${response.status}] ${JSON.stringify(response.data)}`); console.log(`Response in getDataDayAheadExaa(): [${response.status}] ${JSON.stringify(response.data)}`); if (response.data.data) resolve(response.data.data.h); else resolve(null); } }) .catch(error => { if (error?.response?.data) { console.error('Error in getDataDayAheadExaa(): ' + error + ' with response ' + JSON.stringify(error.response.data)); this.log.error('Error to get market price (Exaa) ' + error + ' with response ' + JSON.stringify(error.response.data)); } else { console.error('Error in getDataDayAheadExaa(): ' + error); this.log.error('Error to get market price (Exaa) ' + error); } if (error?.response?.status >= 500) resolve(null); else reject(error); }); }); } /** * Retrieves marketdata from REST-API from Exaa * @param {string} country country of the market */ async getDataDayAheadExaa1015(country) { country = country.toUpperCase(); const day0 = cleanDate(new Date()); const day = addDays(day0, 1); const dateStringToday = `${day.getFullYear()}-${day.getMonth() + 1}-${day.getDate()}`; const uri = `https://www.exaa.at/data/market-results?delivery_day=${dateStringToday}&market=${country}&auction=1015`; this.log.debug(`API-Call ${uri}`); console.log(`API-Call ${uri}`); return new Promise((resolve, reject) => { axiosInstance.get(uri) .then((response) => { if (!response || !response.data) { throw new Error(`getDataDayAheadExaa1015(): Respone empty for URL ${uri} with status code ${response.status}`); } else { this.log.debug(`Response in getDataDayAheadExaa1015(): [${response.status}] ${JSON.stringify(response.data)}`); console.log(`Response in getDataDayAheadExaa1015(): [${response.status}] ${JSON.stringify(response.data)}`); if (response.data && response.data.AT && response.data.AT.price) { if (country == 'AT') resolve(response.data.AT.price); else resolve(response.data.DE.price); } else resolve(null); } }) .catch(error => { if (error?.response?.data) { console.error('Error in getDataDayAheadExaa1015(): ' + error + ' with response ' + JSON.stringify(error.response.data)); this.log.error('Error to get market price (Exaa1015) ' + error + ' with response ' + JSON.stringify(error.response.data)); } else { console.error('Error in getDataDayAheadExaa1015(): ' + error); this.log.error('Error to get market price (Exaa1015) ' + error); } if (error?.response?.status >= 500) resolve(null); else reject(error); }); }); } /** * Retrieves marketdata from REST-API from Awattar * @param {boolean} tomorrow * @param {string} country country of the market */ async getDataDayAheadAwattar(tomorrow, country) { const day0 = cleanDate(new Date()); let start = 0; let end = 0; if (tomorrow) { let day1 = addDays(day0, 1); start = day1.getTime(); day1.setHours(23, 59, 59); end = day1.getTime() + 2000; } else { start = day0.getTime(); day0.setHours(23, 59, 59); end = day0.getTime() + 2000; } let uri = ''; if (country == 'at') uri = `https://api.awattar.at/v1/marketdata?start=${start}&end=${end}`; else uri = `https://api.awattar.de/v1/marketdata?start=${start}&end=${end}`; this.log.debug(`API-Call ${uri}`); console.log(`API-Call ${uri}`); return new Promise((resolve, reject) => { axiosInstance.get(uri) .then((response) => { if (!response || !response.data) { throw new Error(`getDataDayAheadAwattar(): Respone empty for URL ${uri} with status code ${response.status}`); } else { this.log.debug(`Response in getDataDayAheadAwattar(): [${response.status}] ${JSON.stringify(response.data)}`); console.log(`Response in getDataDayAheadAwattar(): [${response.status}] ${JSON.stringify(response.data)}`); resolve(response.data); } }) .catch(error => { if (error?.response?.data) { console.error('Error in getDataDayAheadAwattar(): ' + error + ' with response ' + JSON.stringify(error.response.data)); this.log.error('Error to get market price (Awattar) ' + error + ' with response ' + JSON.stringify(error.response.data)); } else { console.error('Error in getDataDayAheadAwattar(): ' + error); this.log.error('Error to get market price (Awattar) ' + error); } if (error?.response?.status >= 500) resolve(null); else reject(error); }); }); } /** * Retrieves marketdata from REST-API from entsoe * @param {boolean} tomorrow * @param {string} country country of the market */ async getDataDayAheadEntsoe(tomorrow, country) { const url = 'https://web-api.tp.entsoe.eu/api?documentType=A44'; const securityToken = this.token; const day0 = cleanDate(new Date()); const day1 = addDays(day0, 1); let day = new Date(); if (tomorrow) day = day1; else day = day0; let dayPlus = addDays(day, 1); const datebegin = day.getFullYear() + pad(day.getMonth() + 1, 2) + pad(day.getDate(), 2); const dateend = dayPlus.getFullYear() + pad(dayPlus.getMonth() + 1, 2) + pad(dayPlus.getDate(), 2); let domain = ''; switch (country) { case 'ch': domain = '10YCH-SWISSGRIDZ'; break; case 'at': domain = '10YAT-APG------L'; break; case 'de': domain = '10Y1001A1001A82H'; break; default: this.log.error('Country not found in definitions'); } const uri = `${url}&securityToken=${securityToken}&periodStart=${datebegin}0000&periodEnd=${dateend}0000&in_Domain=${domain}&Out_Domain=${domain}`; this.log.debug(`API-Call ${uri}`); console.log(`API-Call ${uri}`); return new Promise((resolve, reject) => { axiosInstance.get(uri) .then((response) => { if (!response || !response.data) { throw new Error(`getDataDayAheadEntsoe(): Respone empty for URL ${uri} with status code ${response.status}`); } else { this.log.debug(`Response in getDataDayAheadEntsoe(): [${response.status}] ${JSON.stringify(response.data)}`); console.log(`Response in getDataDayAheadEntsoe(): [${response.status}] ${JSON.stringify(response.data)}`); let result = xml2js(response.data); if (result.Publication_MarketDocument) resolve((result.Publication_MarketDocument)); else resolve(null); } }) .catch(error => { if (error?.response?.data) { console.error('Error in getDataDayAheadEntsoe(): ' + error + ' with response ' + JSON.stringify(error.response.data)); this.log.warn('Error to get market price (Entsoe) ' + error + ' with response ' + JSON.stringify(error.response.data)); } else { console.error('Error in getDataDayAheadEntsoe(): ' + error); this.log.warn('Error to get market price (Entsoe) ' + error); } reject(error); }); }); } /** * Handles json-object and creates states for market prices * @param {string} country * @param {boolean} forecast */ async ExecuteRequestDayAhead(country, forecast) { let source1 = '', source0 = ''; try { let prices0Awattar, prices1Awattar, prices0Exaa, prices1Exaa, prices1Exaa1015, prices0Entsoe, prices1Entsoe; const day0 = cleanDate(new Date()); const day1 = addDays(day0, 1); jsonExplorer.stateSetCreate('marketprice.today.date', 'date', day0.getTime()); jsonExplorer.stateSetCreate('marketprice.tomorrow.date', 'date', day1.getTime()); let prices0 = [], prices1 = []; let tomorrow = false; if (country == 'ch') { try { tomorrow = false; prices0Entsoe = await this.getDataDayAheadEntsoe(tomorrow, country); tomorrow = true; prices1Entsoe = await this.getDataDayAheadEntsoe(tomorrow, country); } catch (error) { if (String(error).includes('read ECONNRESET') || String(error).includes('timeout') || String(error).includes('socket hang up')) { this.log.info(`Let's wait 3 minutes and try again...`); await jsonExplorer.sleep(3 * 60 * 1000); this.log.info(`OK! Let's try again now!`); if (tomorrow == false) { prices0Entsoe = await this.getDataDayAheadEntsoe(tomorrow, country); tomorrow = true; prices1Entsoe = await this.getDataDayAheadEntsoe(tomorrow, country); } else { prices1Entsoe = await this.getDataDayAheadEntsoe(tomorrow, country); } } else throw error; } this.log.debug('Today ' + JSON.stringify(prices0Entsoe)); this.log.debug('Tomorrow ' + JSON.stringify(prices1Entsoe)); //Convert Etsoe-structure to Exaa-structure for today and tomorrow if (prices0Entsoe) { this.log.debug('prices0Entsoe: ' + JSON.stringify(prices0Entsoe)); console.log('prices0Entsoe: ' + JSON.stringify(prices0Entsoe)); if (prices0Entsoe.TimeSeries[0] == null && prices0Entsoe.TimeSeries == null) { this.log.error('No data available for today!'); return; } let point = []; if (prices0Entsoe.TimeSeries[0]?.Period[0]?.Point) point = prices0Entsoe.TimeSeries[0].Period[0].Point; else if (prices0Entsoe.TimeSeries[0]?.Period?.Point) point = prices0Entsoe.TimeSeries[0].Period.Point; else if (prices0Entsoe.TimeSeries?.Period[0]?.Point) point = prices0Entsoe.TimeSeries.Period[0].Point; else if (prices0Entsoe.TimeSeries?.Period?.Point) point = prices0Entsoe.TimeSeries.Period.Point; else { console.error('Received data for today did not fit to supported patterns! Received data: ' + JSON.stringify(prices0Entsoe)); throw new Error('Received data for today did not fit to supported patterns! Received data: ' + JSON.stringify(prices0Entsoe)); } let length = point.length; for (let i = 0; i < length; i++) { let ii = String(i); prices0[ii] = {}; let price = parseFloat(point[i].price_amount._text); let sHour = pad(point[i].position._text, 2); prices0[ii].Price = price; prices0[ii].Product = 'H' + sHour; } jsonExplorer.stateSetCreate('marketprice.today.source', 'Source', 'entsoe'); } if (prices1Entsoe) { this.log.debug('prices1Entsoe: ' + JSON.stringify(prices1Entsoe)); console.log('prices1Entsoe: ' + JSON.stringify(prices1Entsoe)); if (prices1Entsoe.TimeSeries[0] == null && prices1Entsoe.TimeSeries == null) { this.log.error('No data available for tomorrow!'); return; } let point = []; if (prices1Entsoe.TimeSeries[0]?.Period[0]?.Point) point = prices1Entsoe.TimeSeries[0].Period[0].Point; else if (prices1Entsoe.TimeSeries[0]?.Period?.Point) point = prices1Entsoe.TimeSeries[0].Period.Point; else if (prices1Entsoe.TimeSeries?.Period[0]?.Point) point = prices1Entsoe.TimeSeries.Period[0].Point; else if (prices1Entsoe.TimeSeries?.Period?.Point) point = prices1Entsoe.TimeSeries.Period.Point; else throw new Error('Received data for tomorrow did not fit to supported patterns! Received data: ' + JSON.stringify(prices1Entsoe)); let length = point.length; for (let i = 0; i < length; i++) { let ii = String(i); prices1[ii] = {}; let price = parseFloat(point[i].price_amount._text); let sHour = pad(point[i].position._text, 2); prices1[ii].Price = price; prices1[ii].Product = 'H' + sHour; } jsonExplorer.stateSetCreate('marketprice.tomorrow.source', 'Source', 'entsoe'); } } else { prices0Awattar = await this.getDataDayAheadAwattar(false, country); if (prices0Awattar && prices0Awattar.data) this.log.debug(`Day ahead result for Awattar today is: ${JSON.stringify(prices0Awattar.data)}`); else this.log.debug(`Day ahead result for Awattar today is: NO DATA`); if (!prices0Awattar || !prices0Awattar.data || !prices0Awattar.data[0]) { this.log.info(`No prices from Awattar for today, let's try Exaa`); prices0Exaa = await this.getDataDayAheadExaa(false, country); this.log.debug(`Day ahead result for Exaa today is: ${JSON.stringify(prices0Exaa)}`); if (!prices0Exaa) this.log.warn('No market data for today'); } prices1Awattar = await this.getDataDayAheadAwattar(true, country); if (prices1Awattar && prices1Awattar.data) this.log.debug(`Day ahead result for Awattar tomorrow is: ${JSON.stringify(prices1Awattar.data)}`); else this.log.debug(`Day ahead result for Awattar tomorrow is: NO DATA`); if (!prices1Awattar || !prices1Awattar.data || !prices1Awattar.data[0]) { this.log.info(`No prices from Awattar for tomorrow, let's try Exaa`); prices1Exaa = await this.getDataDayAheadExaa(true, country); this.log.debug(`Day ahead result for Exaa tomorrow is: ${JSON.stringify(prices1Exaa)}`); if (!prices1Exaa && forecast) { this.log.info('No prices from Exaa MC, last change Exaa 10.15 auction'); prices1Exaa1015 = await this.getDataDayAheadExaa1015(country); this.log.debug(`Day ahead result for Exaa1015 tomorrow is: ${JSON.stringify(prices1Exaa1015)}`); } } //Convert Awattar-structure to Exaa-structure for today prices0 = []; if (prices0Exaa) { prices0 = prices0Exaa; source0 = 'exaaMC'; jsonExplorer.stateSetCreate('marketprice.today.source', 'Source', source0); } else { if (prices0Awattar && prices0Awattar.data && prices0Awattar.data[0]) { for (const idS in prices0Awattar.data) { prices0[idS] = {}; prices0[idS].Price = prices0Awattar.data[idS].marketprice; let start = new Date(prices0Awattar.data[idS].start_timestamp); let iHour = start.getHours() + 1; let sHour = pad(iHour, 2); prices0[idS].Product = 'H' + sHour; } source0 = 'awattar'; jsonExplorer.stateSetCreate('marketprice.today.source', 'Source', source0); } } //Convert structures to Exaa-structure for tomorrow prices1 = []; if (prices1Exaa) { prices1 = prices1Exaa; source1 = 'exaaMC'; jsonExplorer.stateSetCreate('marketprice.tomorrow.source', 'Source', source1); } else { if (prices1Exaa1015) { for (const idS in prices1Exaa1015) { prices1[idS] = {}; prices1[idS].Price = prices1Exaa1015[idS].y; let iHour = prices1Exaa1015[idS].x; let sHour = pad(iHour, 2); prices1[idS].Product = 'H' + sHour; } this.log.debug('prices1Exaa1015 converted to: ' + JSON.stringify(prices1)); source1 = 'exaa1015'; jsonExplorer.stateSetCreate('marketprice.tomorrow.source', 'Source', source1); } else if (prices1Awattar && prices1Awattar.data && prices1Awattar.data[0]) { for (const idS in prices1Awattar.data) { prices1[idS] = {}; prices1[idS].Price = prices1Awattar.data[idS].marketprice; let start = new Date(prices1Awattar.data[idS].start_timestamp); let iHour = start.getHours() + 1; let sHour = pad(iHour, 2); prices1[idS].Product = 'H' + sHour; } source1 = 'awattar'; jsonExplorer.stateSetCreate('marketprice.tomorrow.source', 'Source', source1); } } } if (prices0) await jsonExplorer.TraverseJson(prices0, 'marketprice.details.today', true, true); else await jsonExplorer.TraverseJson(null, 'marketprice.details.today', true, true); if (prices1) await jsonExplorer.TraverseJson(prices1, 'marketprice.details.tomorrow', true, true); else await jsonExplorer.TraverseJson(null, 'marketprice.details.tomorrow', true, true); let jDay0 = {}, jDay1 = {}, jDay0BelowThreshold = {}, jDay1BelowThreshold = {}, jDay0AboveThreshold = {}, jDay1AboveThreshold = {}; let days0Above = 0, days0Below = 0, days1Above = 0, days1Below = 0; //manage today (day0) for (const idS in prices0) { if (prices0[idS].Price == undefined) { this.log.error('No marketprice found in marketprice-result for today!'); return 'error'; } let product = prices0[idS].Product; let marketprice = this.calcPrice(prices0[idS].Price / 10); this.log.debug('Marketprice for product ' + product + ' is ' + marketprice); let sEndHour = product.substring(1, 3); let iEndHour = Number(sEndHour); let iBeginHour = iEndHour - 1; let sBeginHour = pad(iBeginHour, 2); let range = sBeginHour + '_to_' + sEndHour; jDay0[range] = marketprice; if (marketprice < threshold) { jDay0BelowThreshold[range] = marketprice; days0Below++; } else { jDay0AboveThreshold[range] = marketprice; days0Above++; } } this.log.debug('Day0 looks like ' + JSON.stringify(jDay0)); //manage tomorrow (day1) for (const idS in prices1) { if (prices1[idS].Price == undefined) { this.log.error('No marketprice found in marketprice-result for tomorrow!'); return 'error'; } let product = prices1[idS].Product; let marketprice = this.calcPrice(prices1[idS].Price / 10); this.log.debug('Marketprice for product ' + product + ' is ' + marketprice); let sEndHour = product.substring(1, 3); let iEndHour = Number(sEndHour); let iBeginHour = iEndHour - 1; let sBeginHour = pad(iBeginHour, 2); let range = sBeginHour + '_to_' + sEndHour; jDay1[range] = marketprice; if (marketprice < threshold) { jDay1BelowThreshold[range] = marketprice; days1Below++; } else { jDay1AboveThreshold[range] = marketprice; days1Above++; } } this.log.debug('Day1 looks like ' + JSON.stringify(jDay1)); //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]]); jDay0BelowThreshold.numberOfHours = days0Below; jDay0AboveThreshold.numberOfHours = days0Above; jDay1BelowThreshold.numberOfHours = days1Below; jDay1AboveThreshold.numberOfHours = days1Above; this.createChart(arrAll0, arrAll1, source1); this.log.debug('Marketprice jDay0: ' + JSON.stringify(jDay0)); this.log.debug('Marketprice jDay0BelowThreshold: ' + JSON.stringify(jDay0BelowThreshold)); this.log.debug('Marketprice jDay0AboveThreshold: ' + JSON.stringify(jDay0AboveThreshold)); this.log.debug('Marketprice jDay1: ' + JSON.stringify(jDay1)); this.log.debug('Marketprice jDay1AboveThreshold: ' + JSON.stringify(jDay1AboveThreshold)); this.log.debug('Marketprice jDay1BelowThreshold: ' + JSON.stringify(jDay1BelowThreshold)); await jsonExplorer.traverseJson(jDay0, 'marketprice.today', true, true); await jsonExplorer.traverseJson(jDay0BelowThreshold, 'marketprice.belowThreshold.today', true, true); await jsonExplorer.traverseJson(jDay0AboveThreshold, 'marketprice.aboveThreshold.today', true, true); await jsonExplorer.traverseJson(jDay1, 'marketprice.tomorrow', true, true); await jsonExplorer.traverseJson(jDay1BelowThreshold, 'marketprice.belowThreshold.tomorrow', true, true); await jsonExplorer.traverseJson(jDay1AboveThreshold, 'marketprice.aboveThreshold.tomorrow', true, true); //now it is time to sort by prcie arrBelow0.sort(compareSecondColumn); arrBelow1.sort(compareSecondColumn); arrAll0.sort(compareSecondColumn); arrAll1.sort(compareSecondColumn); //prepare sorted arrays to create states let sortedHours0 = [], sortedHours1 = [], sortedHoursAll0 = [], sortedHoursAll1 = []; let sortedHours0Short = [], sortedHours1Short = [], sortedHours0ShortAll = [], sortedHours1ShortAll = []; let priceSum0 = 0, priceSum1 = 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 arrBelow1) { sortedHours1[idS] = [arrBelow1[idS][0], arrBelow1[idS][1]]; sortedHours1Short[idS] = Number(arrBelow1[idS][0].substring(0, 2)); } 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 arrAll1) { sortedHoursAll1[idS] = [arrAll1[idS][0], arrAll1[idS][1]]; sortedHours1ShortAll[idS] = Number(arrAll1[idS][0].substring(0, 2)); priceSum1 = priceSum1 + arrAll1[idS][1]; } let price0Avg, price1Avg; 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; await jsonExplorer.traverseJson(sortedHours0, 'marketprice.belowThreshold.today_sorted', true, true); await jsonExplorer.traverseJson(sortedHours1, 'marketprice.belowThreshold.tomorrow_sorted', true, true); await jsonExplorer.traverseJson(sortedHoursAll0, 'marketprice.today_sorted', true, true); await jsonExplorer.traverseJson(sortedHoursAll1, 'marketprice.tomorrow_sorted', true, true); await jsonExplorer.stateSetCreate('marketprice.belowThreshold.today_sorted.short', 'today sorted short', JSON.stringify(sortedHours0Short)); await jsonExplorer.stateSetCreate('marketprice.belowThreshold.tomorrow_sorted.short', 'tomorrow sorted short', JSON.stringify(sortedHours1Short)); await jsonExplorer.stateSetCreate('marketprice.today_sorted.short', 'today sorted short', JSON.stringify(sortedHours0ShortAll)); await jsonExplorer.stateSetCreate('marketprice.tomorrow_sorted.short', 'tomorrow sorted short', JSON.stringify(sortedHours1ShortAll)); await jsonExplorer.stateSetCreate('marketprice.today.average', 'average', price0Avg); await jsonExplorer.stateSetCreate('marketprice.tomorrow.average', 'average', price1Avg); await jsonExplorer.checkExpire('marketprice.*'); await jsonExplorer.deleteObjectsWithNull('marketprice.*Threshold.*'); await jsonExplorer.deleteObjectsWithNull('marketprice.details.*'); } catch (error) { let eMsg = `Error in ExecuteRequestDayAhead(): ${error}`; this.log.error(eMsg); console.error(eMsg); this.sendSentry(error); } } /** * Handles json-object and creates states for peak hours */ async ExecuteRequestPeakHours() { try { let result = await this.getDataPeakHours(); 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 (!result.StatusInfos[idS].utc) { this.log.error('No UTC found in peak-result!'); return 'error'; } this.log.debug(result.StatusInfos[idS].utc); iHour = new Date(result.StatusInfos[idS].utc).getHours(); if (iHour < 9) sHour = 'from_0' + String(iHour) + '_to_' + '0' + String(iHour + 1); else if (iHour == 9) sHour = 'from_0' + String(iHour) + '_to_' + String(iHour + 1); else sHour = 'from_' + String(iHour) + '_to_' + String(iHour + 1); let dateToCheck = cleanDate(new Date(result.StatusInfos[idS].utc)); if (dateToCheck.getTime() == day0.getTime()) jDay0[sHour] = new Date(result.StatusInfos[idS].utc).getTime(); else if (dateToCheck.getTime() == day1.getTime()) jDay1[sHour] = new Date(result.StatusInfos[idS].utc).getTime(); else if (dateToCheck.getTime() == day2.getTime()) jDay2[sHour] = new Date(result.StatusInfos[idS].utc).getTime(); else if (dateToCheck.getTime() == day3.getTime()) jDay3[sHour] = new Date(result.StatusInfos[idS].utc).getTime(); else if (dateToCheck.getTime() == day4.getTime()) jDay4[sHour] = new Date(result.StatusInfos[idS].utc).getTime(); if (i < 10) jDayAll['item 0' + i] = new Date(result.StatusInfos[idS].utc).getTime(); else jDayAll['item ' + i] = new Date(result.StatusInfos[idS].utc).getTime(); i = i + 1; } this.log.debug('Peak jDay0: ' + JSON.stringify(jDay0)); this.log.debug('Peak jDay1: ' + JSON.stringify(jDay1)); this.log.debug('Peak jDay2: ' + JSON.stringify(jDay2)); this.log.debug('Peak jDay3: ' + JSON.stringify(jDay3)); this.log.debug('Peak jDay4: ' + JSON.stringify(jDay4)); this.log.debug('Peak jDayAll: ' + JSON.stringify(jDayAll)); await jsonExplorer.traverseJson(jDay0, 'peakTime.today', true, true); await jsonExplorer.traverseJson(jDay1, 'peakTime.today+1', true, true); await jsonExplorer.traverseJson(jDay2, 'peakTime.today+2', true, true); await jsonExplorer.traverseJson(jDay3, 'peakTime.today+3', true, true); await jsonExplorer.traverseJson(jDay4, 'peakTime.today+4', true, true); await jsonExplorer.traverseJson(jDayAll, 'peakTime.allDays', true, true); await jsonExplorer.checkExpire('peakTime.*'); jsonExplorer.deleteObjectsWithNull('peakTime.*'); /* // check for outdated states to be deleted let statesToDelete = await this.getStatesAsync('peakTime.*'); for (const idS in statesToDelete) { let state = await this.getStateAsync(idS); if (state && state.val == null) { this.log.debug(`State "${idS}" will be deleted`); await this.delObjectAsync(idS); } }*/ } catch (error) { let eMsg = `Error in ExecuteRequestPeakHours(): ${error})`; this.log.error(eMsg); console.error(eMsg); this.sendSentry(error); } } /** * Creates JSON-date for charts for today and tomorrow */ async createChart(arrayToday, arrayTomorrow, sourceTomorrow) { let todayData = []; let tomorrowData = []; let chart = {}; let todayMin = 1000, tomorrowMin = 1000; let todayMax = 0, tomorrowMax = 0; for (const idS in arrayToday) { let iHour = parseInt(arrayToday[idS][0]); //analysing "00_to_01" with parseInt ignores everything starting with "_" todayData[idS] = { 'y': arrayToday[idS][1], 't': calcDate(iHour, false) }; todayMin = Math.min(todayMin, Number(arrayToday[idS][1])); todayMax = Math.max(todayMax, Number(arrayToday[idS][1])); } for (const idS in arrayTomorrow) { let iHour = parseInt(arrayTomorrow[idS][0]); //analysing "00_to_01" with parseInt ignores everything starting with "_" tomorrowData[idS] = { 'y': arrayTomorrow[idS][1], 't': calcDate(iHour, true) }; tomorrowMin = Math.min(tomorrowMin, Number(arrayTomorrow[idS][1])); tomorrowMax = Math.max(tomorrowMax, Number(arrayTomorrow[idS][1])); } let allMin = Math.min(todayMin, tomorrowMin); let allMax = Math.max(todayMax, tomorrowMax); allMax = Math.ceil(allMax * 1.1 / 5) * 5; let todayMaxIndex = todayData.length - 1; let tomorrowMaxIndex = tomorrowData.length - 1; if (todayData[todayMaxIndex] && todayData[todayMaxIndex].y && todayData[todayMaxIndex].t) todayData[todayMaxIndex + 1] = { 'y': todayData[todayMaxIndex].y, 't': todayData[todayMaxIndex].t + 60 * 60 * 1000 }; if (tomorrowData[tomorrowMaxIndex] && tomorrowData[tomorrowMaxIndex].y && tomorrowData[tomorrowMaxIndex].t) tomorrowData[tomorrowMaxIndex + 1] = { 'y': tomorrowData[tomorrowMaxIndex].y, 't': tomorrowData[tomorrowMaxIndex].t + 60 * 60 * 1000 }; chart.graphs = []; chart.graphs[0] = {}; chart.graphs[0].type = 'line'; chart.graphs[0].color = 'gray'; chart.graphs[0].line_steppedLine = true; chart.graphs[0].xAxis_timeFormats = { 'hour': 'HH' }; chart.graphs[0].xAxis_time_unit = 'hour'; chart.graphs[0].yAxis_min = Math.min(0, allMin); chart.graphs[0].yAxis_max = allMax; chart.graphs[0].datalabel_show = 'auto'; chart.graphs[0].datalabel_minDigits = 2; chart.graphs[0].datalabel_maxDigits = 2; chart.graphs[0].xAxis_bounds = 'data'; chart.graphs[0].line_pointSize = 5; chart.graphs[0].line_PointColor = 'rgba(0, 0, 0, 0)'; chart.graphs[0].datalabel_fontSize = 10; chart.graphs[0].datalabel_color = 'black'; chart.graphs[0].line_UseFillColor = true; chart.graphs[0].data = todayData; await jsonExplorer.stateSetCreate('marketprice.today.jsonChart', 'jsonChart', JSON.stringify(chart)); chart.graphs[0].data = tomorrowData; if (sourceTomorrow == 'exaa1015') chart.graphs[0].color = 'lightgray'; await jsonExplorer.stateSetCreate('marketprice.tomorrow.jsonChart', 'jsonChart', JSON.stringify(chart)); } /** * Handles sentry message * @param {any} errorObject Error message for sentry */ sendSentry(errorObject) { if (errorObject?.message?.includes('ETIMEDOUT')) return; try { if (this.supportsFeature && this.supportsFeature('PLUGINS')) { const sentryInstance = this.getPluginInstance('sentry'); if (sentryInstance?.getSentryObject()) { sentryInstance.getSentryObject().captureException(errorObject); } } } catch (error) { this.log.error(`Error in function sendSentry() main.js: ${error}`); } } /** * @param {number} tradePrice * @returns {number} */ calcPrice(tradePrice) { tradePrice = Math.round(tradePrice * 1000) / 1000; let price = 0; if (this.calculate == true) { let provider = Math.abs(tradePrice * this.feeRelative) + this.feeAbsolute; let charges = (tradePrice + provider) * this.charges; let vat = (tradePrice + provider + charges + this.gridCosts) * this.vat; price = tradePrice + provider + charges + this.gridCosts + vat; } else price = tradePrice; price = Math.round(price * 1000) / 1000; this.log.debug('tradePrice is ' + tradePrice + ' and finalPrice is ' + price); return price; } } // @ts-ignore parent is a valid property on module if (module.parent) { // Export the constructor in compact mode /** * @param {Partial<utils.AdapterOptions>} [options={}] */ module.exports = (options) => new ApgInfo(options); } else { // otherwise start the instance directly new ApgInfo(); } /**************************************************** */ /* H E L P E R S */ /**************************************************** */ /** * sets time to 00:00:00.00000 * @param {Date} date date to be changed */ function cleanDate(date) { date.setHours(0, 0, 0, 0); return date; } /** * @param {number} hour * @param {boolean} tomorrow * @returns {number} */ function calcDate(hour, tomorrow = false) { let date = cleanDate(new Date()); if (tomorrow) date = addDays(date, 1); date.setHours(hour); return date.getTime(); } /** * adds days to a date * @param {Date} date origin date * @param {number} numberOfDays number of days which origin date shall be added (positive and negative allowes) */ function addDays(date, numberOfDays) { let newDate = new Date(date.getTime()); newDate.setDate(newDate.getDate() + numberOfDays); return (cleanDate(newDate)); } function compareSecondColumn(a, b) { if (a[1] === b[1]) { return 0; } else { return (a[1] < b[1]) ? -1 : 1; } } /** * @param {number} num number * @param {number} length length * @returns {string} returns a string with leading zeros based on given number */ function pad(num, length) { if (num == null) num = 0; let l = Math.floor(length); let sn = String(num); let snl = sn.length; if (snl >= l) return sn; return '0'.repeat(l - snl) + sn; } /** * @param {string} xmlString * @returns {object} */ function xml2js(xmlString) { // @ts-ignore xmlString = xmlString.replaceAll(`price.amount`, `price_amount`); const jsonResult = JSON.parse(convert.xml2json(xmlString, { compact: true, })); return (jsonResult); }