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
JavaScript
'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