UNPKG

iobroker.energiefluss-erweitert

Version:

This ioBroker adapter is the next Version of the energiefluss with the attribute 'erweitert' and allows to display an energyflow animation inside an iframe or HTML DIV.

1,160 lines (1,039 loc) 93.1 kB
'use strict'; /* * Created with @iobroker/create-adapter v2.3.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'); const fs = require('fs'); const path = require('path'); const systemDictionary = require('./lib/dictionary.js'); let instanceDir; const backupDir = '/backup'; /* Variables for runtime */ let globalConfig = {}; let sourceObject = {}; let settingsObj = {}; let rawValues = {}; let outputValues = { values: {}, unit: {}, animations: {}, fillValues: {}, borderValues: {}, prepend: {}, append: {}, css: {}, override: {}, img_href: {}, }; let relativeTimeCheck = {}; let globalInterval; let subscribeArray = []; let systemLang = 'en'; class EnergieflussErweitert extends utils.Adapter { constructor(options) { super({ ...options, name: 'energiefluss-erweitert', useFormatDate: true, }); this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); // this.on('objectChange', this.onObjectChange.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { // Language systemLang = this.language ?? 'en'; // Initialize your adapter here /* Create Adapter Directory - Backup */ instanceDir = utils.getAbsoluteInstanceDataDir(this); if (!fs.existsSync(instanceDir + backupDir)) { fs.mkdirSync(instanceDir + backupDir, { recursive: true }); } /* Check, if we have an old backup state */ let tmpBackupState = await this.getStateAsync('backup'); if (tmpBackupState) { let tmpBackup = JSON.parse(tmpBackupState.val); this.log.info('Migrating old backup to new strategy. Please wait!'); if (Object.keys(tmpBackup).length > 0) { this.log.info(`${Object.keys(tmpBackup).length} Backup's found. Converting!`); for (var key of Object.keys(tmpBackup)) { let datetime = key; let [date, time] = datetime.split(', '); let [day, month, year] = date.split('.'); let [hour, minutes, seconds] = time.split(':'); let fileName = new Date(year, month - 1, day, hour, minutes, seconds).getTime(); const newFilePath = path.join(instanceDir + backupDir, `BACKUP_${fileName}.json`); fs.writeFile(newFilePath, JSON.stringify(tmpBackup[key]), err => { if (err) { this.log.error(`Could not create Backup ${newFilePath}. Error: ${err}`); } }); } } // After creation of new backup - delete the state this.log.info('Convertion of backups finished'); } // Delete old Objects this.delObjectAsync('backup'); this.delObjectAsync('battery_remaining'); this.log.info('Adapter started. Loading config!'); this.getConfig(); } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * * @param callback Callback function */ onUnload(callback) { try { // Here you must clear all timeouts or intervals that may still be active this.clearInterval(globalInterval); this.log.info('Cleared interval for relative values!'); callback(); } catch { callback(); } } /** * Is called if a subscribed state changes * * @param id ID of the state * @param state State itself */ async onStateChange(id, state) { // The state was changed if (id && state) { // The state is acknowledged if (state.ack) { this.log.debug('Refreshing ACK state from foreign state!'); await this.refreshData(id, state); } // For userdata and Javascript if ( id.toLowerCase().startsWith('0_userdata.') || id.toLowerCase().startsWith('javascript.') || id.toLowerCase().startsWith('alias.') ) { this.log.debug(`Refreshing state from user environment! ${id}`); await this.refreshData(id, state); } } } /** * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... * Using this method requires "common.messagebox" property to be set to true in io-package.json * * @param obj Message Object */ async onMessage(obj) { //this.log.debug(`[onMessage] received command: ${obj.command} with message: ${JSON.stringify(obj.message)}`); if (obj && obj.message) { if (typeof obj.message === 'object') { // Request the list of Backups let fileList = []; switch (obj.command) { case '_getBackups': { const listBackups = path.join(instanceDir + backupDir); fs.readdir(listBackups, (err, files) => { if (err) { this.sendTo(obj.from, obj.command, { err }, obj.callback); } else { files.forEach(file => { let tmpFile = path.parse(file).name; tmpFile = tmpFile.replace('BACKUP_', ''); fileList.push(tmpFile); }); this.sendTo(obj.from, obj.command, { error: null, data: fileList }, obj.callback); } }); break; } case '_restoreBackup': { // Restore Backup this.log.info('Starting restoring Backup from disk!'); const restorePath = path.join(instanceDir + backupDir, `BACKUP_${obj.message.filename}.json`); fs.readFile(restorePath, 'utf8', (err, data) => { if (err) { this.log.info(`Error during ${err}`); this.sendTo(obj.from, obj.command, { error: err }, obj.callback); } else { // Send new config back to workspace and store in state this.setStateChangedAsync('configuration', { val: data, ack: true, }); this.sendTo( obj.from, obj.command, { error: null, data: JSON.parse(data) }, obj.callback, ); this.log.info('Backup restored and activated!'); } }); break; } case '_saveConfiguration': { // Store Backup this.log.debug('Saving Backup to disk!'); const filename = new Date().getTime(); const storePath = path.join(instanceDir + backupDir, `BACKUP_${filename}.json`); // Get current configuration const tmpConfig = await this.getStateAsync('configuration'); fs.writeFile(storePath, tmpConfig.val, err => { if (err) { this.sendTo(obj.from, obj.command, { err }, obj.callback); } else { this.sendTo( obj.from, obj.command, { error: null, data: 'Backup stored successfully!' }, obj.callback, ); } }); // Store the new configuration in state await this.setStateChangedAsync('configuration', { val: JSON.stringify(obj.message), ack: true, }); // Recycle old Backups fileList = []; fs.readdir(instanceDir + backupDir, (err, files) => { if (!err) { files.forEach(file => { fileList.push(file); }); // Walk through the list an delete all files after index 9 if (fileList.length > 10) { // Order the List fileList.sort((a, b) => -1 * a.localeCompare(b)); for (let i = 10; i < fileList.length; i++) { fs.unlink(`${instanceDir}${backupDir}/${fileList[i]}`, err => { if (err) { this.log.warn(err); } this.log.info(`${fileList[i]} successfully deleted!`); }); } } else { this.log.info( 'The amount of current stored backups does not exceed the number of 10!', ); } } }); break; } case '_updateElementInView': { // Receive Object from ioBroker to show it in Configuration const originID = obj.message.id; const id = `tmp_${originID}`; const originSource = obj.message.source; const state = await this.getForeignStateAsync(originSource); let objectUnit = ''; rawValues[id] = state.val; // Modify the source obj.message.source = id; if (state) { // Get the type if auto if (obj.message.source_display == 'auto') { const type = await this.getForeignObjectAsync(originSource); obj.message.source_type = type.common.type; objectUnit = type.common.unit; } await this.calculateValue(id, obj.message, state); this.log.debug(`Found ${obj.message.source} and calculated the value for Web-ID: ${id}!`); if (Object.hasOwn(outputValues.values, id)) { let returnObj = { values: {}, unit: {}, override: {}, }; returnObj.values[originID] = outputValues.values[id]; returnObj.unit[originID] = objectUnit; returnObj.override[originID] = outputValues.override[id]; this.sendTo( obj.from, obj.command, { error: null, data: returnObj, }, obj.callback, ); // Delete temporary values delete outputValues.override[id]; delete outputValues.values[id]; delete rawValues[id]; } else { this.sendTo( obj.from, obj.command, { error: 'There was an error, while getting the updated value!', }, obj.callback, ); } } break; } default: this.log.warn( `[onMessage] Received command "${obj.command}" via 'sendTo', which is not implemented!`, ); this.sendTo( obj.from, obj.command, { error: `Received command "${obj.command}" via 'sendTo', which is not implemented!`, }, obj.callback, ); break; } } else { this.log.error(`[onMessage] Received incomplete message via 'sendTo'`); if (obj.callback) { this.sendTo(obj.from, obj.command, { error: 'Incomplete message' }, obj.callback); } } } } /** * Converts minutes to a string representation of hours and minutes. * * @param mins - The number of minutes to convert. * @returns The string representation of the hours and minutes. */ getMinHours(mins) { const m = mins % 60; const h = (mins - m) / 60; return `${h < 10 ? '0' : ''}${h.toString()}:${m < 10 ? '0' : ''}${m.toString()}`; } /** * Calculate the value of a given element. * * @param id - The id of the element. * @param obj - The object containing the source and settings for the element. * @param state - The state of the element. * @returns Value */ async calculateValue(id, obj, state) { // prettier-ignore const factor = globalConfig?.datasources && Object.hasOwn(globalConfig.datasources, obj.source) ? (globalConfig.datasources[obj.source].factor ?? 1) : 1; let sourceValue = rawValues[obj.source] * factor; this.log.debug( `Values for: ${id} - Using source: ${obj.source} rawValue: ${ rawValues[obj.source] } sourceValue: ${sourceValue} Settings: ${JSON.stringify(obj)}`, ); // Decide, which type we have switch (obj.type) { case 'image': // Check, if we have a static picture or one via state if (obj.href) { let tmpImg = await this.getForeignStateAsync(obj.href); outputValues.img_href[id] = tmpImg.val || '#'; this.log.debug( `Loading Image for ${id} with: ${JSON.stringify(obj)} Result: ${outputValues.img_href[id]}`, ); } break; case 'circle': case 'rect': // Element is not Text - It is Rect or Circle if (obj.fill_type != -1 && obj.fill_type) { outputValues.fillValues[id] = sourceValue; } if (obj.border_type != -1 && obj.border_type) { outputValues.borderValues[id] = sourceValue; } break; case 'text': if (obj.source_option != -1 || obj.source_option_lc != -1) { let timeStamp; if (obj.source_option != -1) { this.log.debug( `Source Option 'last update' detected! ${obj.source_option} Generating DateString for ${ state.ts } ${this.getTimeStamp(state.ts, obj.source_option)}`, ); timeStamp = this.getTimeStamp(state.ts, obj.source_option); } if (obj.source_option_lc != -1) { this.log.debug( `Source Option 'last change' detected! ${obj.source_option_lc} Generating DateString for ${ state.lc } ${this.getTimeStamp(state.lc, obj.source_option_lc)}`, ); timeStamp = this.getTimeStamp(state.lc, obj.source_option_lc); } outputValues.values[id] = timeStamp; } else { const checkDisplay = async method => { switch (method) { case 'auto': switch (obj.source_type) { case 'boolean': checkDisplay('bool'); break; case 'number': checkDisplay(''); break; case 'string': checkDisplay('text'); break; } break; case 'text': { // Linebreak Option let strOutput; if (obj.linebreak > 0 && state.val && state.val.toString().length > 0) { let splitOpt = new RegExp( `.{0,${obj.linebreak}}(?:\\s|$)|.{0,${obj.linebreak}}`, 'g', ); let splitted = state.val.toString().match(splitOpt); strOutput = splitted.join('<br>'); } else { strOutput = state.val; } outputValues.values[id] = strOutput; sourceValue = strOutput; } break; case 'bool': outputValues.values[id] = sourceValue ? systemDictionary['on'][systemLang] : systemDictionary['off'][systemLang]; break; case 'own_text': outputValues.values[id] = obj.text; break; default: // Threshold need to be positive if (obj.threshold >= 0) { this.log.debug(`Threshold for: ${id} is: ${obj.threshold}`); // Check, if we have Subtractions for this value const subArray = obj.subtract; let subValue = 0; if (Array.isArray(subArray) && subArray.length > 0 && subArray[0] != -1) { subValue = subArray.reduce( (acc, idx) => acc - rawValues[idx] * (globalConfig.datasources[idx].factor ?? 1), 0, ); this.log.debug(`Subtracted by: ${subArray.toString()}`); // Set the subtraction state await this.setStateChangedAsync(`calculation.elements.element_${id}.subtract`, { val: Number(sourceValue) + Number(subValue), ack: true, }); } // Check, if we have Additions for this value const addArray = obj.add; let addValue = 0; if (Array.isArray(addArray) && addArray.length > 0 && addArray[0] != -1) { addValue = addArray.reduce( (acc, idx) => acc + rawValues[idx] * (globalConfig.datasources[idx].factor ?? 1), 0, ); this.log.debug(`Added to Value: ${addArray.toString()}`); // Set the addition state await this.setStateChangedAsync(`calculation.elements.element_${id}.addition`, { val: Number(sourceValue) + Number(addValue), ack: true, }); } let formatValue = Number(sourceValue) + Number(subValue) + Number(addValue); // Check if value is over threshold if (Math.abs(formatValue) >= obj.threshold) { // Convert Value to positive let cValue = obj.convert ? Math.abs(formatValue) : formatValue; // Calculation switch (obj.calculate_kw) { case 'calc': case true: // Convert to kW if set cValue = Math.round((cValue / 1000) * 100) / 100; break; case 'auto': if (Math.abs(cValue) >= 1000000000) { outputValues.unit[id] = 'GW'; // Convert to GW if set cValue = Math.round((cValue / 1000000000) * 100) / 100; } else if (Math.abs(cValue) >= 1000000) { outputValues.unit[id] = 'MW'; // Convert to MW if set cValue = Math.round((cValue / 1000000) * 100) / 100; } else if (Math.abs(cValue) >= 1000) { outputValues.unit[id] = 'kW'; // Convert to kW if set cValue = Math.round((cValue / 1000) * 100) / 100; } else { outputValues.unit[id] = 'W'; } break; case 'none': case false: break; default: return cValue; } outputValues.values[id] = obj.decimal_places >= 0 ? this.decimalPlaces(cValue, obj.decimal_places) : cValue; } else { outputValues.values[id] = obj.decimal_places >= 0 ? this.decimalPlaces(0, obj.decimal_places) : sourceValue; } } break; } }; checkDisplay(obj.source_display); } break; } // Overrides for elements if (obj.override) { this.log.debug( `Gathering override for ID: ${id}, Processed-value: ${sourceValue}, Raw-value: ${ rawValues[obj.source] }, Override: ${JSON.stringify(obj.override)}`, ); outputValues.override[id] = await this.getOverridesAsync(sourceValue, obj.override); } } /** * Converts a duration in milliseconds to a human-readable time string. * * @param duration - The duration in milliseconds. * @returns The human-readable time string. */ msToTime(duration) { const seconds = Math.floor((duration / 1000) % 60); const minutes = Math.floor((duration / (1000 * 60)) % 60); const hours = Math.floor((duration / (1000 * 60 * 60)) % 24); let value = systemDictionary['timer_now'][systemLang]; if (hours > 0) { if (hours < 5 && hours >= 2) { value = systemDictionary['timer_few_hours'][systemLang]; } else if (hours == 1) { value = this.sprintf(systemDictionary['timer_hour_ago'][systemLang], hours); } else { value = this.sprintf(systemDictionary['timer_hours_ago'][systemLang], hours); } return value; } if (minutes > 0) { if (minutes < 5 && minutes >= 2) { value = systemDictionary['timer_few_minutes'][systemLang]; } else if (minutes == 1) { value = this.sprintf(systemDictionary['timer_minute_ago'][systemLang], minutes); } else { value = this.sprintf(systemDictionary['timer_minutes_ago'][systemLang], minutes); } return value; } if (seconds > 0) { if (seconds < 5 && seconds >= 2) { value = systemDictionary['timer_few_seconds'][systemLang]; } else if (seconds == 1) { value = this.sprintf(systemDictionary['timer_second_ago'][systemLang], seconds); } else { value = this.sprintf(systemDictionary['timer_seconds_ago'][systemLang], seconds); } } return value; } /** * Replaces occurrences of `%s` in the given format string with the corresponding * elements from the arguments array. * * @param format - The format string with `%s` placeholders. * @returns The formatted string with placeholders replaced by the corresponding values. */ sprintf(format) { var args = Array.prototype.slice.call(arguments, 1); var i = 0; return format.replace(/%s/g, function () { return args[i++]; }); } /** * Returns a formatted timestamp based on the given mode. * * @param ts - The timestamp in milliseconds. * @param mode - The mode to determine the format of the timestamp. * @returns The formatted timestamp. */ getTimeStamp(ts, mode) { if (!ts || ts <= 0) { return ''; } const date = new Date(ts); switch (mode) { case 'timestamp_de': default: return date.toLocaleString('de-DE', { hour: 'numeric', minute: 'numeric', day: '2-digit', month: '2-digit', year: 'numeric', second: '2-digit', hour12: false, }); case 'timestamp_de_short': return date.toLocaleString('de-DE', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', year: '2-digit', hour12: false, }); case 'timestamp_de_short_wo_year': return date.toLocaleString('de-DE', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', hour12: false, }); case 'timestamp_de_hhmm': return date.toLocaleString('de-DE', { hour: '2-digit', minute: '2-digit', }); case 'timestamp_us': return date.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', day: '2-digit', month: '2-digit', year: 'numeric', second: '2-digit', hour12: true, }); case 'timestamp_us_short': return date.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', year: '2-digit', hour12: true, }); case 'timestamp_us_short_wo_year': return date.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', hour12: true, }); case 'timestamp_us_hhmm': return date.toLocaleString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: true, }); case 'relative': { const now = new Date(); return this.msToTime(now - date); } case 'ms': return ts; } } /** * Converts a given timestamp into a formatted date and time string. * * @param ts - The timestamp in milliseconds. * @returns A string representing the date and time in the format "DD.MM.YYYY HH:MM:SS", * or an empty string if the timestamp is invalid or non-positive. */ getDateTime(ts) { if (!ts || ts <= 0) { return ''; } const date = new Date(ts); const pad = n => n.toString().padStart(2, '0'); // Hilfsfunktion für führende Nullen const day = pad(date.getDate()); const month = pad(date.getMonth() + 1); const year = date.getFullYear(); const hours = pad(date.getHours()); const minutes = pad(date.getMinutes()); const seconds = pad(date.getSeconds()); return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`; } /** * Maps a given object of source-objects to their corresponding relative times. * * @param obj An object with the following structure: * - Each key is the name of the object which is to be created. * - Each value is an object with the following structure: * - source: The source object to use. * - option: The option to use when generating the timestamp. If -1, the value is not used. * (See getTimeStamp for possible values) * - option_lc: The option to use when generating the timestamp from the last change. If -1, the value is not used. * (See getTimeStamp for possible values) * @returns A Promise which resolves when all objects have been processed. */ async getRelativeTimeObjects(obj) { const keys = Object.keys(obj); const promises = keys.map(async key => { const stateValue = await this.getForeignStateAsync(obj[key].source); if (stateValue) { let timeStamp; if (obj[key].option != -1) { timeStamp = this.getTimeStamp(stateValue.ts, obj[key].option); } if (obj[key].option_lc != -1) { timeStamp = this.getTimeStamp(stateValue.lc, obj[key].option_lc); } outputValues.values[key] = timeStamp; } }); await Promise.all(promises); } /** * Returns the given value as a string, rounded to the specified number of decimal places. * * @param value - The value to round. * @param decimal_places - The number of decimal places to round to. * @returns The rounded value as a string. */ decimalPlaces(value, decimal_places) { return Number(value).toFixed(decimal_places); } /** * Calculates the duration (in ms) that a power meter animation should run based on the maximum duration, maximum power, and current power. * The result is limited to 60000 (1 minute) if necessary. * * @param maxDuration - The maximum duration of the animation in ms. * @param maxPower - The maximum power of the power meter. * @param currentPower - The current power of the power meter. * @returns The duration of the animation in ms. */ calculateDuration(maxDuration, maxPower, currentPower) { // Max Duration let cur = Number(currentPower); let max = Number(maxPower); let dur = Number(maxDuration); // Calculate the result and limit it to 60000 if necessary return Math.min(Math.round((max / cur) * dur), 60000); } /** * Calculates the stroke dash array for an SVG path element based on the maximum number of dots, maximum power, and current power. * * @param maxDots - The maximum number of dots to be drawn. * @param maxPower - The maximum power. * @param currentPower - The current power. * @returns The stroke dash array for the SVG path element. */ calculateStrokeArray(maxDots, maxPower, currentPower) { const totalLength = 136; const l_distance = globalConfig.animation_configuration.distance; const l_length = globalConfig.animation_configuration.length; // Calculate the number of dots to be drawn let l_amount = Math.round((currentPower / maxPower) * maxDots); l_amount = Math.min(l_amount, maxDots); // Initialize stroke dash array let strokeDash = ''; let total = totalLength; if (l_amount > 0 && l_length > 0 && l_distance > 0) { for (let i = 0; i < l_amount; i++) { strokeDash += `${l_length} `; if (i !== l_amount - 1) { strokeDash += `${l_distance} `; total -= l_distance; } total -= l_length; } strokeDash += ` ${total < 0 ? l_distance : total}`; } else { strokeDash = `${l_length} ${totalLength - l_length}`; } return strokeDash; } async replacePlaceholders(value) { const dpRegex = /\{([^{}]+\.[^{}]+\.[^{}]+)\}/g; const matches = [...value.matchAll(dpRegex)]; for (const match of matches) { this.log.debug(`Trying to receive: ${match[1]}`); const state = await this.getForeignStateAsync(match[1]); this.log.debug(`Received: ${JSON.stringify(state)} via ${match[1]}`); // State found if (state) { value = value.replace(match[0], state.val); } else { return { status: false, error: `Error: State <b>${match[1]}</b> not found!`, function: value, }; } } return value; } evaluateConditions(workObj, condValue) { let tmpWorker = {}; // Property not found. We need to check the values! const operators = new RegExp('[=><!]'); Object.keys(workObj) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase(), undefined, { numeric: true, sensitivity: 'base', }), ) .forEach(item => { if (operators.test(item)) { // Now, we need to check, if condValue is a number if (!isNaN(condValue)) { // Operator found - check for condition try { const func = Function(`return ${condValue}${item} `)(); if (func) { tmpWorker = workObj[item]; } } catch (func) { tmpWorker.error = { status: false, error: func.toString(), function: condValue + item, }; } } } }); return tmpWorker; } async getOverridesAsync(condValue, obj) { let tmpWorker = {}; let workObj; try { workObj = typeof obj === 'string' ? JSON.parse(obj) : JSON.parse(JSON.stringify(obj)); } catch { return tmpWorker; } if (Object.hasOwn(workObj, condValue)) { // Check, if Property exists - if yes, directly return it, because thats the best match return workObj[condValue]; } tmpWorker = this.evaluateConditions(workObj, condValue); if (Object.keys(tmpWorker).length == 0 && Object.hasOwn(workObj, 'default')) { // Check, if we have a default fallback for it tmpWorker = workObj['default']; } if (typeof tmpWorker == 'string') { // Return the Object as Error return { error: { status: false, error: 'You need to provide an object for this condition to work! Visit the <a href="https://github.com/SKB-CGN/ioBroker.energiefluss-erweitert/wiki/Custom-Overrides-for-elements" target="_blank">Wiki</a> for help!', function: 'No function executed, due to missing object', }, }; } // Now we process the found values inside tmpWorker Obj if (Object.keys(tmpWorker).length > 0) { for (const item of Object.keys(tmpWorker)) { // Temp Storage of workerValue let itemToWorkWith = tmpWorker[item]; // Check if we are not destroying the error object if (typeof itemToWorkWith != 'object') { itemToWorkWith = itemToWorkWith.toString(); const replacedItems = await this.replacePlaceholders(itemToWorkWith); if (replacedItems?.status === false) { tmpWorker[item] = replacedItems; break; } else { this.log.debug(`Running function: ${itemToWorkWith}`); itemToWorkWith = replacedItems; try { const func = new Function(`return ${itemToWorkWith} `)(); tmpWorker[item] = func(condValue); } catch (func) { if (itemToWorkWith.includes('=>')) { tmpWorker[item] = { status: false, error: func.toString(), function: itemToWorkWith, }; } else { tmpWorker[item] = itemToWorkWith; } } } } } } return tmpWorker; } /** * @param id ID of the state * @param state State itself */ async refreshData(id, state) { if (id == `${this.namespace}.configuration`) { this.log.info('Configuration changed via Workspace! Reloading config!'); this.getConfig(); return; } let cssRules = []; // Check, if we handle this source inside our subscribtion if (Object.hasOwn(sourceObject, id)) { // sourceObject for this state-id const soObj = sourceObject[id]; // Number for calculation const stateValue = state.val; // prettier-ignore const factor = globalConfig?.datasources && Object.hasOwn(globalConfig.datasources, soObj.id) ? (globalConfig.datasources[soObj.id].factor ?? 1) : 1; const calcNumber = (typeof state.val === 'string' ? Number(state.val.replace(/[^\d.-]/g, '')) : state.val) * factor; // Check, if the value has been updated - if not, dont refresh it this.log.debug(`Current Value of ${id}: ${stateValue} - saved Value: ${rawValues[soObj.id]}`); if (stateValue == rawValues[soObj.id]) { this.log.debug(`Value of ${id} did not change. Ignoring!`); return; } this.log.debug( `Value of ${id} changed! Old Value: ${rawValues[soObj.id]} | New Value: ${stateValue} Processing!`, ); // Put Value into RAW-Source-Values rawValues[soObj.id] = stateValue; // Runner for calculating the values const sourceRunner = async what => { this.log.debug(`Updated through ${what}: ${JSON.stringify(rawValues)}`); // Run through the provided object for (const key of Object.keys(soObj[what])) { const elmID = soObj[what][key]; if (what == 'elmSources') { // Put ID into CSS-Rule for later use cssRules.push(elmID); } if (Object.hasOwn(settingsObj, elmID)) { this.log.debug(`Value-Settings for Element ${elmID} found! Applying Settings!`); await this.calculateValue(elmID, settingsObj[elmID], state); } } }; // Loop through each addSource if (Object.hasOwn(soObj, 'addSources') && soObj['addSources'].length) { await sourceRunner('addSources'); } // Loop through each subtractSource if (Object.hasOwn(soObj, 'subtractSources') && soObj['subtractSources'].length) { await sourceRunner('subtractSources'); } // Loop through each Element, which belongs to that source if (Object.hasOwn(soObj, 'elmSources') && soObj['elmSources'].length) { await sourceRunner('elmSources'); } // Check, if that Source belongs to battery-charge or discharge, to determine the time if (Object.hasOwn(globalConfig, 'calculation')) { // Check, if the provided source is a valid source const isValidDatasource = value => { if (value === null || value === undefined || value === '') { return false; } // Check, if value is type 'number' if (typeof value !== 'number') { return false; } // Check, if value is greater than or equal 0 ist return !isNaN(value) && Number(value) >= 0; }; // Battery Remaining if (Object.hasOwn(globalConfig.calculation, 'battery')) { const batObj = globalConfig.calculation.battery; const isRelevantId = soObj.id == batObj.charge || soObj.id == batObj.discharge; if ( isRelevantId && isValidDatasource(batObj.charge) && isValidDatasource(batObj.discharge) && isValidDatasource(batObj.percent) ) { let direction = 'none'; let energy = 0; const batteryValue = Math.abs(calcNumber); const setDirectionAndEnergy = (dir, en) => { direction = dir; energy = en; }; if (batObj.charge !== batObj.discharge) { if (soObj.id === batObj.charge) { setDirectionAndEnergy('charge', batteryValue); } if (soObj.id === batObj.discharge) { setDirectionAndEnergy('discharge', batteryValue); } } else { if (calcNumber > 0) { if (!batObj.charge_prop) { setDirectionAndEnergy('charge', batteryValue); } if (!batObj.discharge_prop) { setDirectionAndEnergy('discharge', batteryValue); } } else if (calcNumber < 0) { if (batObj.charge_prop) { setDirectionAndEnergy('charge', batteryValue); } if (batObj.discharge_prop) { setDirectionAndEnergy('discharge', batteryValue); } } } // Calculate the rest time of the battery this.getForeignStateAsync(globalConfig.datasources[batObj.percent].source) .then(state => { const capacity = isValidDatasource(batObj.capacity) ? rawValues[batObj.capacity] * (globalConfig.datasources[batObj.capacity].factor ?? 1) : 0; const dod = isValidDatasource(batObj.dod) ? rawValues[batObj.dod] * (globalConfig.datasources[batObj.dod].factor ?? 1) : 0; const percent = state.val; let rest = 0; let mins = 0; let string = '--:--h'; let target = 0; const batt_energy = (capacity * (percent - dod)) / 100 || 0; if (percent > 0 && energy > 0) { if (direction === 'charge') { rest = capacity - (capacity * percent) / 100; } else if (direction === 'discharge') { rest = (capacity * (percent - dod)) / 100; } mins = Math.round((rest / energy) * 60); if (mins > 0) { string = `${this.getMinHours(mins)}h`; target = Math.floor(Date.now() / 1000) + mins * 60; } } this.log.debug( `Direction: ${direction} Time to fully ${direction}: $Percent: ${percent} Energy: ${energy} Rest