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,163 lines (1,045 loc) • 98 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('node:fs');
const path = require('node: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';
// Password
this.password = this.config.password;
if (this.password.toString().length > 0) {
this.log.info('Workspace configuration is password protected!');
} else {
this.log.warn('Workspace configuration is NOT password protected!');
}
// 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, { error: 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 '_checkProtection': {
this.sendTo(
obj.from,
obj.command,
{ error: null, data: this.password && this.password.toString().length > 0 ? true : false },
obj.callback,
);
break;
}
case '_checkPassword': {
if (obj.message.password == this.password.toString()) {
this.sendTo(obj.from, obj.command, { error: null, data: true }, obj.callback);
} else {
this.sendTo(obj.from, obj.command, { error: 'Password is wrong!' }, obj.callback);
}
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 = originSource ? await this.getForeignStateAsync(originSource) : null;
// Modify the source
obj.message.source = id;
if (state && state.val != null) {
let objectUnit = '';
rawValues[id] = state.val;
// Get the type if auto
if (obj.message.source_display == 'auto') {
const type = await this.getForeignObjectAsync(originSource);
obj.message.source_type = type.common.type;
if (obj.message.calculate_kw == 'none') {
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 || outputValues.unit[id];
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,
);
}
} else {
this.sendTo(
obj.from,
obj.command,
{
error: 'State does not exist!',
},
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) {
default:
outputValues.values[id] = sourceValue;
break;
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()} with Value: ${subValue} | Result: ${sourceValue + subValue}`,
);
// 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 by: ${addArray.toString()} with Value: ${addValue} | Result: ${sourceValue + addValue}`,
);
// Set the addition state
await this.setStateChangedAsync(`calculation.elements.element_${id}.addition`, {
val: Number(sourceValue) + Number(addValue),
ack: true,
});
}
const 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;
outputValues.unit[id] = 'kW';
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:
outputValues.unit[id] = '';
break;
default:
return cValue;
}
this.log.debug(`Calculated Value for: ${id} is: ${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;
}
};
await 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
const cur = Number(currentPower);
const max = Number(maxPower);
const dur = Number(maxDuration);
// Calculate the result and limit it to 60000 if necessary
return Math.min(Math.round((max / cur) * dur), 60000);
}
calculateStrokeDots(maxDots, maxPower, currentPower) {
if (currentPower <= 0) {
return 0;
}
// Calculate the number of dots to be drawn
const amount = Math.round((currentPower / maxPower) * maxDots);
return Math.max(1, Math.min(amount, maxDots));
}
async replacePlaceholders(value) {
//const dpRegex = /\{([^{}]+\.[^{}]+\.[^{}]+)\}/g; /* Old Regex without number */
const dpRegex = /\{([^{}]*\.\d+\.[^{}]+)\}/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) {
// Property not found. We need to check the values!
const operators = new RegExp('[=><!]');
const sortedKeys = Object.keys(workObj).sort((a, b) => {
const parse = s => {
const match = s.match(/^([<>]=?|!?=)?\s*(-?\d+(\.\d+)?)/);
if (!match) {
return { op: '', val: 0 };
}
return { op: match[1] || '', val: parseFloat(match[2]) };
};
const A = parse(a);
const B = parse(b);
if (A.op.startsWith('>')) {
return B.val - A.val;
}
if (A.op.startsWith('<')) {
return A.val - B.val;
}
return 0;
});
// Numeric condValue
const isNumeric = !isNaN(condValue);
const errorFunc = 'No function executed!';
const errorWiki =
'Visit the <a href="https://www.kreyenborg.koeln/wissensdatenbank/ueberschreibungen/" target="_blank">Wiki</a> for help!';
for (const item of sortedKeys) {
// Pre-Check for condition set
if (typeof workObj[item] != 'object') {
// Check, if condition set is not an object
return {
error: {
status: false,
error: `You need to provide an object with overrides for this condition to work! ${errorWiki}`,
function: errorFunc,
},
};
}
if (typeof workObj[item] == 'object' && Object.keys(workObj[item]).length == 0) {
// Check, if condition set is an object and not empty
return {
error: {
status: false,
error: `The provided set of overrides is empty! ${errorWiki}`,
function: errorFunc,
},
};
}
// Now, we need to check, if condValue is a number and we have an operator
if (operators.test(item) && isNumeric) {
// Operator found - check for condition
try {
const func = Function(`return ${condValue}${item} `)();
if (func) {
return workObj[item];
}
} catch (func) {
return {
error: {
status: false,
error: func.toString(),
function: condValue + item,
},
};
}
} else if (item == condValue) {
// No Operator found - Check for property
return workObj[item];
}
}
// Still no return - check for default value
if ('default' in workObj) {
return workObj['default'];
}
// Return empty
return {};
}
async getOverridesAsync(condValue, obj) {
let workObj;
try {
workObj = typeof obj === 'string' ? JSON.parse(obj) : JSON.parse(JSON.stringify(obj));
} catch {
return {};
}
// Generate object for conditions
const tmpWorker = this.evaluateConditions(workObj, condValue);
// 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);
this.log.debug(`Result of the function: ${tmpWorker[item]}`);
// Check, if we have an undefined result
if (typeof tmpWorker[item] == 'undefined') {
tmpWorker[item] = {
status: false,
error: 'Your function returned <b>undefined</b>! Did you use a return statement? Example: <code>return someValue;</code>',
function: itemToWorkWith,
};
}
} catch (func) {
// Test for incomplete function
const hasBraces = /\{\s*[^{}]*\s*\}/;
const hasArrow = /=>/;
if (hasBraces.test(itemToWorkWith) && !hasArrow.test(itemToWorkWith)) {
this.log.debug(`Error of the function: ${func.toString()}`);
tmpWorker[item] = {
status: false,
error: 'You can not run a function block directly! You need to open a function block with an anonymous function first! <code>Example: () => {}</code>',
function: itemToWorkWith,
};
} else if (hasArrow.test(itemToWorkWith)) {
this.log.debug(`Error of the function: ${func.toString()}`);
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)
) {