iobroker.javascript
Version:
Rules Engine for ioBroker
1,175 lines (1,076 loc) • 179 kB
JavaScript
/// <reference path="./javascript.d.ts" />
/* jslint global: console */
/* eslint-env node */
'use strict';
const { isObject, isArray, promisify, getHttpRequestConfig } = require('./tools');
const utils = require('@iobroker/adapter-core');
const pattern2RegEx = utils.commonTools.pattern2RegEx;
// let context = {
// adapter,
// mods,
// errorLogFunction,
// subscriptions,
// subscribedPatterns,
// states,
// adapterSubs,
// objects,
// cacheObjectEnums,
// stateIds,
// logWithLineInfo,
// timers,
// enums,
// channels,
// devices,
// isEnums,
// getAbsoluteDefaultDataDir,
// };
/**
* @typedef {Object} SandboxContext
* @property {Record<string, string[]>} channels
* @property {Record<string, string[]>} devices
* @property {string[]} stateIds
*/
/**
* @param {{[prop: string]: any} & SandboxContext} context
*/
function sandBox(script, name, verbose, debug, context) {
const consts = require('./consts');
const words = require('./words');
const eventObj = require('./eventObj');
const patternCompareFunctions = require('./patternCompareFunctions');
const jsonata = require('jsonata');
/** @type {ioBroker.Adapter} */
const adapter = context.adapter;
const mods = context.mods;
const states = context.states;
const objects = context.objects;
const timers = context.timers;
const enums = context.enums;
const debugMode = context.debugMode;
function errorInCallback(e) {
adapter.setState(`scriptProblem.${name.substring('script.js.'.length)}`, { val: true, ack: true, c: 'errorInCallback' });
context.logError('Error in callback', e);
context.debugMode && console.log(`error$$${name}$$Exception in callback: ${e}`, Date.now());
}
function subscribePattern(script, pattern) {
if (adapter.config.subscribe) {
if (!script.subscribes[pattern]) {
script.subscribes[pattern] = 1;
} else {
script.subscribes[pattern]++;
}
if (!context.subscribedPatterns[pattern]) {
context.subscribedPatterns[pattern] = 1;
adapter.subscribeForeignStates(pattern);
// request current value to deliver old value on change.
if (typeof pattern === 'string' && !pattern.includes('*')) {
adapter.getForeignState(pattern, (err, state) => {
if (state) {
states[pattern] = state;
}
});
} else {
adapter.getForeignStates(pattern, (err, _states) =>
_states && Object.keys(_states).forEach(id => states[id] = _states[id]));
}
} else {
context.subscribedPatterns[pattern]++;
}
}
}
function unsubscribePattern(script, pattern) {
if (adapter.config.subscribe) {
if (script.subscribes[pattern]) {
script.subscribes[pattern]--;
if (!script.subscribes[pattern]) {
delete script.subscribes[pattern];
}
}
if (context.subscribedPatterns[pattern]) {
context.subscribedPatterns[pattern]--;
if (!context.subscribedPatterns[pattern]) {
adapter.unsubscribeForeignStates(pattern);
delete context.subscribedPatterns[pattern];
// if pattern was regex or with * some states will stay in RAM, but it is OK.
if (states[pattern]) {
delete states[pattern];
}
}
}
}
}
function subscribeFile(script, id, fileNamePattern) {
const key = `${id}$%$${fileNamePattern}`;
if (!script.subscribesFile[key]) {
script.subscribesFile[key] = 1;
} else {
script.subscribesFile[key]++;
}
if (!context.subscribedPatternsFile[key]) {
context.subscribedPatternsFile[key] = 1;
adapter.subscribeForeignFiles(id, fileNamePattern);
} else {
context.subscribedPatternsFile[key]++;
}
}
function unsubscribeFile(script, id, fileNamePattern) {
const key = `${id}$%$${fileNamePattern}`;
if (script.subscribesFile[key]) {
script.subscribesFile[key]--;
if (!script.subscribesFile[key]) {
delete script.subscribesFile[key];
}
}
if (context.subscribedPatternsFile[key]) {
context.subscribedPatternsFile[key]--;
if (!context.subscribedPatternsFile[key]) {
adapter.unsubscribeForeignFiles(id, fileNamePattern);
delete context.subscribedPatternsFile[key];
}
}
}
/**
* @typedef PatternCompareFunctionArray
* @type {Array<any> & {logic?: string}}
*/
function getPatternCompareFunctions(pattern) {
let func;
/** @type {PatternCompareFunctionArray} */
const functions = [];
functions.logic = pattern.logic || 'and';
for (const key in pattern) {
if (!Object.prototype.hasOwnProperty.call(pattern, key)) {
continue;
}
if (key === 'logic') {
continue;
}
if (key === 'change' && pattern.change === 'any') {
continue;
}
if (!(func = patternCompareFunctions[key])) {
continue;
}
if (typeof (func = func(pattern)) !== 'function') {
continue;
}
functions.push(func);
}
return functions;
}
/** @typedef {{attr: string, value: string, idRegExp?: RegExp}} Selector */
/**
* Splits a selector string into attribute and value
* @param {string} selector The selector string to split
* @returns {Selector}
*/
function splitSelectorString(selector) {
const parts = selector.split('=', 2);
if (parts[1] && parts[1][0] === '"') {
parts[1] = parts[1].substring(1);
const len = parts[1].length;
if (parts[1] && parts[1][len - 1] === '"') {
parts[1] = parts[1].substring(0, len - 1);
}
}
if (parts[1] && parts[1][0] === "'") {
parts[1] = parts[1].substring(1);
const len = parts[1].length;
if (parts[1] && parts[1][len - 1] === "'") {
parts[1] = parts[1].substring(0, len - 1);
}
}
if (parts[1]) {
parts[1] = parts[1].trim();
}
parts[0] = parts[0].trim();
return {attr: parts[0], value: parts[1]};
}
/**
* Transforms a selector string with wildcards into a regular expression
* @param {string} str The selector string to transform into a regular expression
*/
function selectorStringToRegExp(str) {
const startsWithWildcard = str[0] === '*';
const endsWithWildcard = str[str.length - 1] === '*';
// Sanitize the selector, so it is safe to use in a RegEx
// Taken from https://stackoverflow.com/a/3561711/10179833 but modified
// since * has a special meaning in our selector and should not be escaped
// eslint-disable-next-line no-useless-escape
str = str.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*');
return new RegExp(
(startsWithWildcard ? '' : '^')
+ str
+ (endsWithWildcard ? '' : '$')
);
}
/**
* Adds a regular expression for selectors targeting the state ID
* @param {Selector} selector The selector to apply the transform to
* @returns {Selector}
*/
function addRegExpToIdAttrSelectors(selector) {
if (
(selector.attr === 'id' || selector.attr === 'state.id')
&& (!selector.idRegExp && selector.value)
) {
return {
attr: selector.attr,
value: selector.value,
idRegExp: selectorStringToRegExp(selector.value)
};
} else {
return selector;
}
}
/**
* Tests if a value loosely equals (==) the reference string.
* In contrast to the equality operator, this treats true == "true" as well
* so we can test common and native attributes for boolean values
* @param {boolean | string | number | undefined} value The value to compare with the reference
* @param {string} reference The reference to compare the value to
*/
function looselyEqualsString(value, reference) {
// For booleans, compare the string representation
// For other types do a loose comparison
return typeof value === 'boolean'
? (value && reference === 'true') || (!value && reference === 'false')
: value == reference
;
}
/**
* Returns the `common.type` for a given variable
* @param {any} value
* @returns {iobJS.CommonType}
*/
function getCommonTypeOf(value) {
// @ts-ignore we do not support bigint
return isArray(value) ? 'array'
: (isObject(value) ? 'object'
: typeof value);
}
/**
* Returns if an id is in an allowed namespace for automatic object creations
* @param id {string} id to check
* @returns {boolean}
*/
function validIdForAutomaticFolderCreation(id) {
return id.startsWith('javascript.') || id.startsWith('0_userdata.0.') || id.startsWith('alias.0.');
}
/**
* Iterate through object structure to create missing folder objects
* @param id {string} id
* @returns {Promise<void>}
*/
async function ensureObjectStructure(id) {
if (!validIdForAutomaticFolderCreation(id)) {
return;
}
if (context.folderCreationVerifiedObjects[id] === true) {
return;
}
const idArr = id.split('.');
idArr.pop(); // the last is created as an object in any way
if (idArr.length < 3) {
return; // Nothing to do
}
// We just create sublevel projects
let idToCheck = idArr.splice(0, 2).join('.');
context.folderCreationVerifiedObjects[id] = true;
for (const part of idArr) {
idToCheck += `.${part}`;
if (context.folderCreationVerifiedObjects[idToCheck] === true || objects[idToCheck]) {
continue;
}
context.folderCreationVerifiedObjects[idToCheck] = true;
let obj;
try {
obj = await adapter.getForeignObjectAsync(idToCheck);
} catch (err) {
// ignore
}
if (!obj || !obj.common) {
sandbox.log(`Create folder object for ${idToCheck}`, 'debug');
try {
await adapter.setForeignObjectAsync(idToCheck, {
type: 'folder',
common: {
name: part,
},
native: {
autocreated: 'by automatic ensure logic',
},
});
} catch (err) {
sandbox.log(`Could not automatically create folder object ${idToCheck}: ${err.message}`, 'info');
}
} else {
//sandbox.log(` already existing "${idToCheck}": ${JSON.stringify(obj)}`, 'debug');
}
}
}
function setStateHelper(sandbox, isCreate, isChanged, id, state, isAck, callback) {
if (typeof isAck === 'function') {
callback = isAck;
isAck = undefined;
}
if (state === null) {
state = {val: null};
}
if (isAck === true || isAck === false || isAck === 'true' || isAck === 'false') {
if (isObject(state) && state.val !== undefined) {
// we assume that we were given a state object if
// state is an object that contains a `val` property
state.ack = isAck;
} else {
// otherwise, assume that the given state is the value to be set
state = {val: state, ack: isAck};
}
}
// Check a type of state
if (!objects[id] && objects[`${adapter.namespace}.${id}`]) {
id = `${adapter.namespace}.${id}`;
}
if (isCreate) {
if (id.match(/^javascript\.\d+\.scriptEnabled/)) {
sandbox.log(`Own states (${id}) should not be used in javascript.X.scriptEnabled.*! Please move the states to 0_userdata.0.*`, 'info');
} else if (id.match(/^javascript\.\d+\.scriptProblem/)) {
sandbox.log(`Own states (${id}) should not be used in javascript.X.scriptProblem.*! Please move the states to 0_userdata.0.*`, 'info');
}
}
const common = objects[id] ? objects[id].common : null;
if (common &&
common.type &&
common.type !== 'mixed' &&
common.type !== 'file' &&
common.type !== 'json'
) {
// Find out which type the value has
let actualCommonType;
if (isObject(state)) {
if (state && state.val !== undefined && state.val !== null) {
actualCommonType = getCommonTypeOf(state.val);
}
} else if (state !== null && state !== undefined) {
actualCommonType = getCommonTypeOf(state);
}
// If this is not the expected one, issue a warning
if (actualCommonType && actualCommonType !== common.type) {
context.logWithLineInfo && context.logWithLineInfo.warn(
`You are assigning a ${actualCommonType} to the state "${id}" which expects a ${common.type}. `
+ `Please fix your code to use a ${common.type} or change the state type to ${actualCommonType}. `
+ `This warning might become an error in future versions.`
);
}
if (actualCommonType === 'array' || actualCommonType === 'object') {
try {
if (isObject(state) && typeof state.val !== 'undefined') {
state.val = JSON.stringify(state.val);
} else {
state = JSON.stringify(state);
}
} catch (err) {
context.logWithLineInfo && context.logWithLineInfo.warn(`Could not stringify value for type ${actualCommonType} and id ${id}: ${err.message}`);
if (typeof callback === 'function') {
try {
callback.call(sandbox, `Could not stringify value for type ${actualCommonType} and id ${id}: ${err.message}`);
} catch (e) {
errorInCallback(e);
}
}
}
}
}
// Check min and max of value
if (isObject(state)) {
if (common && typeof state.val === 'number') {
if (common.min !== undefined && state.val < common.min) state.val = common.min;
if (common.max !== undefined && state.val > common.max) state.val = common.max;
}
} else if (common && typeof state === 'number') {
if (common.min !== undefined && state < common.min) state = common.min;
if (common.max !== undefined && state > common.max) state = common.max;
}
// modify state here, to make it available in callback
if (!isObject(state) || state.val === undefined) {
state = {val: state};
state.ack = isAck || false;
}
// we only need this when state cache is used
state = context.prepareStateObject(id, state, isAck);
// set as comment: from which script this state was set.
state.c = sandbox.scriptName;
if (objects[id]) {
script.setStatePerMinuteCounter++;
sandbox.verbose && sandbox.log(`setForeignState(id=${id}, state=${JSON.stringify(state)})`, 'info');
if (debug) {
sandbox.log(`setForeignState(id=${id}, state=${JSON.stringify(state)}) - ${words._('was not executed, while debug mode is active')}`, 'warn');
if (typeof callback === 'function') {
setImmediate(() => {
try {
callback.call(sandbox);
} catch (e) {
errorInCallback(e);
}
});
}
} else {
if (!adapter.config.subscribe) {
// store actual state to make possible to process value in callback
// risk that there will be an error on setState is very low
// but we will not store new state if the setStateChanged is called
if (!isChanged) context.interimStateValues[id] = state;
}
const errHandler = (err, funcId) => {
err && sandbox.log(`${funcId}: ${err}`, 'error');
// If adapter holds all states
if (err && !adapter.config.subscribe) {
delete context.interimStateValues[id];
}
if (typeof callback === 'function') {
setImmediate(() => {
try {
callback.call(sandbox);
} catch (e) {
errorInCallback(e);
}
});
}};
if (isChanged) {
if (!adapter.config.subscribe && context.interimStateValues[id] ) {
// if state is changed, we will compare it with interimStateValues
const oldState = context.interimStateValues[id],
attrs = Object.keys(state).filter(attr => attr !== 'ts' && state[attr] !== undefined);
if (attrs.every(attr => state[attr] === oldState[attr]) === false) {
// state is changed for sure and we will call setForeignState
// and store new state to interimStateValues
context.interimStateValues[id] = state;
adapter.setForeignState(id, state, err => errHandler(err, 'setForeignState'));
} else {
// otherwise - do nothing as we have cached state, except callback
errHandler(null, 'setForeignStateCached');
}
} else {
// adapter not holds all states or it has not cached, then we will simple call setForeignStateChanged
adapter.setForeignStateChanged(id, {...state, ts: undefined}, err => errHandler(err, 'setForeignStateChanged'));
}
} else {
adapter.setForeignState(id, state, err => errHandler(err, 'setForeignState'));
}
}
} else {
context.logWithLineInfo && context.logWithLineInfo.warn(`State "${id}" not found`);
if (typeof callback === 'function') {
setImmediate(() => {
try {
callback.call(sandbox, `State "${id}" not found`);
} catch (e) {
errorInCallback(e);
}
});
}
}
}
const sandbox = {
mods,
_id: script._id,
name, // deprecated
scriptName: name,
instance: adapter.instance,
defaultDataDir: context.getAbsoluteDefaultDataDir(),
verbose,
exports: {}, // Polyfill for the export object in TypeScript modules
require: function (md) {
if (typeof md === 'string' && md.startsWith('node:')) {
md = md.replace(/^node:/, '');
}
if (mods[md]) {
return mods[md];
} else {
try {
mods[md] = require(md);
return mods[md];
} catch (e) {
adapter.setState(`scriptProblem.${name.substring('script.js.'.length)}`, { val: true, ack: true, c: 'require' });
context.logError(name, e, 6);
}
}
},
Buffer: Buffer,
__engine: {
__subscriptionsObject: 0,
__subscriptions: 0,
__subscriptionsMessage: 0,
__subscriptionsFile: 0,
__subscriptionsLog: 0,
__schedules: 0,
},
/**
* @param {string} selector
* @returns {iobJS.QueryResult}
*/
$: function (selector) {
// following is supported
// 'type[commonAttr=something]', 'id[commonAttr=something]', id(enumName="something")', id{nativeName="something"}
// Type can be state, channel or device
// Attr can be any of the common attributes and can have wildcards *
// E.g. "state[id='hm-rpc.0.*]" or "hm-rpc.0.*" returns all states of adapter instance hm-rpc.0
// channel(room="Living room") => all states in room "Living room"
// channel{TYPE=BLIND}[state.id=*.LEVEL]
// Switch all states with .STATE of channels with role "switch" in "Wohnzimmer" to false
// $('channel[role=switch][state.id=*.STATE](rooms=Wohnzimmer)').setState(false);
//
// Following functions are possible, setValue, getValue (only from first), on, each
// Todo CACHE!!!
/** @type {iobJS.QueryResult} */
const result = {};
let name = '';
/** @type {string[]} */
const commonStrings = [];
/** @type {string[]} */
const enumStrings = [];
/** @type {string[]} */
const nativeStrings = [];
let isInsideName = true;
let isInsideCommonString = false;
let isInsideEnumString = false;
let isInsideNativeString = false;
let currentCommonString = '';
let currentNativeString = '';
let currentEnumString = '';
// parse string
let selectorHasInvalidType = false;
if (typeof selector === 'string') {
for (let i = 0; i < selector.length; i++) {
if (selector[i] === '{') {
isInsideName = false;
if (isInsideCommonString || isInsideEnumString || isInsideNativeString) {
// Error
break;
}
isInsideNativeString = true;
} else if (selector[i] === '}') {
isInsideNativeString = false;
nativeStrings.push(currentNativeString);
currentNativeString = '';
} else if (selector[i] === '[') {
isInsideName = false;
if (isInsideCommonString || isInsideEnumString || isInsideNativeString) {
// Error
break;
}
isInsideCommonString = true;
} else if (selector[i] === ']') {
isInsideCommonString = false;
commonStrings.push(currentCommonString);
currentCommonString = '';
} else if (selector[i] === '(') {
isInsideName = false;
if (isInsideCommonString || isInsideEnumString || isInsideNativeString) {
// Error
break;
}
isInsideEnumString = true;
} else if (selector[i] === ')') {
isInsideEnumString = false;
enumStrings.push(currentEnumString);
currentEnumString = '';
} else if (isInsideName) {
name += selector[i];
} else if (isInsideCommonString) {
currentCommonString += selector[i];
} else if (isInsideEnumString) {
currentEnumString += selector[i];
} else if (isInsideNativeString) {
currentNativeString += selector[i];
} //else {
// some error
//}
}
} else {
selectorHasInvalidType = true;
}
// If some error in the selector
if (selectorHasInvalidType || isInsideEnumString || isInsideCommonString || isInsideNativeString) {
result.length = 0;
result.each = function () {
return this;
};
result.getState = function () {
return null;
};
result.setState = function () {
return this;
};
result.on = function () {
return this;
};
}
if (isInsideEnumString) {
sandbox.log(`Invalid selector: enum close bracket ")" cannot be found in "${selector}"`, 'warn');
result.error = 'Invalid selector: enum close bracket ")" cannot be found';
return result;
} else if (isInsideCommonString) {
sandbox.log(`Invalid selector: common close bracket "]" cannot be found in "${selector}"`, 'warn');
result.error = 'Invalid selector: common close bracket "]" cannot be found';
return result;
} else if (isInsideNativeString) {
sandbox.log(`Invalid selector: native close bracket "}" cannot be found in "${selector}"`, 'warn');
result.error = 'Invalid selector: native close bracket "}" cannot be found';
return result;
} else if (selectorHasInvalidType) {
const message = `Invalid selector: selector must be a string but is of type ${typeof selector}`;
sandbox.log(message, 'warn');
result.error = message;
return result;
}
/** @type {Selector[]} */
let commonSelectors = commonStrings.map(selector => splitSelectorString(selector));
let nativeSelectors = nativeStrings.map(selector => splitSelectorString(selector));
const enumSelectorObjects = enumStrings.map(_enum => splitSelectorString(_enum));
const allSelectors = commonSelectors.concat(nativeSelectors, enumSelectorObjects);
// These selectors match the state or object ID and don't belong in the common/native selectors
// Also use RegExp for the ID matching
const stateIdSelectors = allSelectors
.filter(selector => selector.attr === 'state.id')
.map(selector => addRegExpToIdAttrSelectors(selector))
;
const objectIdSelectors = allSelectors
.filter(selector => selector.attr === 'id')
.map(selector => addRegExpToIdAttrSelectors(selector))
;
commonSelectors = commonSelectors.filter(selector => selector.attr !== 'state.id' && selector.attr !== 'id');
nativeSelectors = nativeSelectors.filter(selector => selector.attr !== 'state.id' && selector.attr !== 'id');
const enumSelectors = enumSelectorObjects
.filter(selector => selector.attr !== 'state.id' && selector.attr !== 'id')
// enums are filtered by their enum id, so transform the selector into that
.map(selector => `enum.${selector.attr}.${selector.value}`)
;
name = name.trim();
if (name === 'channel' || name === 'device') {
// Fill the channels and devices objects with the IDs of all their states,
// so we can loop over them afterward
if (!context.channels || !context.devices) {
context.channels = {};
context.devices = {};
for (const _id in objects) {
if (Object.prototype.hasOwnProperty.call(objects, _id) && objects[_id].type === 'state') {
const parts = _id.split('.');
parts.pop();
const chn = parts.join('.');
parts.pop();
const dev = parts.join('.');
context.devices[dev] = context.devices[dev] || [];
context.devices[dev].push(_id);
context.channels[chn] = context.channels[chn] || [];
context.channels[chn].push(_id);
}
}
}
}
if (name === 'schedule') {
if (!context.schedules) {
context.schedules = [];
for (const _id in objects) {
if (Object.prototype.hasOwnProperty.call(objects, _id) && objects[_id].type === 'schedule') {
context.schedules.push(_id);
}
}
}
}
/**
* applies all selectors targeting an object or state ID
* @param {string} objId
* @param {Selector[]} selectors
*/
function applyIDSelectors(objId, selectors) {
// Only keep the ID if it matches every ID selector
return selectors.every(selector =>
!selector.idRegExp || selector.idRegExp.test(objId));
}
/**
* Applies all selectors targeting the Object common properties
* @param {string} objId - The ID of the object in question
*/
function applyCommonSelectors(objId) {
const obj = objects[objId];
if (!obj || !obj.common) return false;
const objCommon = obj.common;
// make sure this object satisfies all selectors
return commonSelectors.every(selector =>
// ensure a property exists
(selector.value === undefined && objCommon[selector.attr] !== undefined)
// or match exact values
|| looselyEqualsString(objCommon[selector.attr], selector.value));
}
/**
* Applies all selectors targeting the Object native properties
* @param {string} objId - The ID of the object in question
*/
function applyNativeSelectors(objId) {
const obj = objects[objId];
if (!obj || !obj.native) return false;
const objNative = obj.native;
// make sure this object satisfies all selectors
return nativeSelectors.every(selector =>
// ensure a property exists
(selector.value === undefined && objNative[selector.attr] !== undefined)
// or match exact values
|| looselyEqualsString(objNative[selector.attr], selector.value));
}
/**
* Applies all selectors targeting the Objects enums
* @param {string} objId - The ID of the object in question
*/
function applyEnumSelectors(objId) {
const enumIds = [];
eventObj.getObjectEnumsSync(context, objId, enumIds);
// make sure this object satisfies all selectors
return enumSelectors.every(_enum => enumIds.includes(_enum));
}
/** @type {string[]} */
let res = [];
if (name === 'schedule') {
res = context.schedules;
if (objectIdSelectors.length) {
res = res.filter(channelId => applyIDSelectors(channelId, objectIdSelectors));
}
// filter out those that don't match every common selector
if (commonSelectors.length) {
res = res.filter(id => applyCommonSelectors(id));
}
// filter out those that don't match every native selector
if (nativeSelectors.length) {
res = res.filter(id => applyNativeSelectors(id));
}
// filter out those that don't match every enum selector
if (enumSelectors.length) {
res = res.filter(channelId => applyEnumSelectors(channelId));
}
} else if (name === 'channel') {
if (!context.channels) {
// TODO: fill the channels and maintain them on all places where context.stateIds will be changed
}
// go through all channels
res = Object.keys(context.channels);
// filter out those that don't match every ID selector for the channel ID
if (objectIdSelectors.length) {
res = res.filter(channelId => applyIDSelectors(channelId, objectIdSelectors));
}
// filter out those that don't match every common selector
if (commonSelectors.length) {
res = res.filter(channelId => applyCommonSelectors(channelId));
}
// filter out those that don't match every native selector
if (nativeSelectors.length) {
res = res.filter(channelId => applyNativeSelectors(channelId));
}
// filter out those that don't match every enum selector
if (enumSelectors.length) {
res = res.filter(channelId => applyEnumSelectors(channelId));
}
// retrieve the state ID collection for all remaining channels
res = res.map(id => context.channels[id])
// and flatten the array to get only the state IDs
.reduce((acc, next) => acc.concat(next), []);
// now filter out those that don't match every ID selector for the state ID
if (stateIdSelectors.length) {
res = res.filter(stateId => applyIDSelectors(stateId, stateIdSelectors));
}
} else if (name === 'device') {
if (!context.devices) {
// TODO: fill the devices and maintain them on all places where context.stateIds will be changed
}
// go through all devices
res = Object.keys(context.devices);
// filter out those that don't match every ID selector for the channel ID
if (objectIdSelectors.length) {
res = res.filter(deviceId => applyIDSelectors(deviceId, objectIdSelectors));
}
// filter out those that don't match every common selector
if (commonSelectors.length) {
res = res.filter(deviceId => applyCommonSelectors(deviceId));
}
// filter out those that don't match every native selector
if (nativeSelectors.length) {
res = res.filter(deviceId => applyNativeSelectors(deviceId));
}
// filter out those that don't match every enum selector
if (enumSelectors.length) {
res = res.filter(deviceId => applyEnumSelectors(deviceId));
}
// retrieve the state ID collection for all remaining devices
res = res.map(id => context.devices[id])
// and flatten the array to get only the state IDs
.reduce((acc, next) => acc.concat(next), []);
// now filter out those that don't match every ID selector for the state ID
if (stateIdSelectors.length) {
res = res.filter(stateId => applyIDSelectors(stateId, stateIdSelectors));
}
} else {
// go through all states
res = context.stateIds;
// if the "name" is not state, then we filter for the ID as well
if (name && name !== 'state') {
const r = new RegExp(`^${name.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`);
res = res.filter(id => r.test(id));
}
// filter out those that don't match every ID selector for the object ID or the state ID
if (objectIdSelectors.length) {
res = res.filter(id => applyIDSelectors(id, objectIdSelectors));
}
// filter out those that don't match every ID selector for the state ID
if (stateIdSelectors.length) {
res = res.filter(id => applyIDSelectors(id, stateIdSelectors));
}
// filter out those that don't match every common selector
if (commonSelectors.length) {
res = res.filter(id => applyCommonSelectors(id));
}
// filter out those that don't match every native selector
if (nativeSelectors.length) {
res = res.filter(id => applyNativeSelectors(id));
}
// filter out those that don't match every enum selector
if (enumSelectors.length) {
res = res.filter(id => applyEnumSelectors(id));
}
}
const resUnique = [...new Set(res)];
for (let i = 0; i < resUnique.length; i++) {
result[i] = resUnique[i];
}
result.length = resUnique.length;
// Implementing the Symbol.iterator contract makes the query result iterable
result[Symbol.iterator] = function*() {
for (let i = 0; i < result.length; i++) {
yield result[i];
}
};
result.each = function (callback) {
if (typeof callback === 'function') {
let r;
for (let i = 0; i < this.length; i++) {
r = callback(result[i], i);
if (r === false) break;
}
}
return this;
};
result.getState = function (callback) {
if (adapter.config.subscribe) {
if (typeof callback !== 'function') {
sandbox.log('You cannot use this function synchronous', 'error');
} else {
adapter.getForeignState(this[0], (err, state) => callback(err, context.convertBackStringifiedValues(this[0], state)));
}
} else {
if (!this[0]) {
return null;
}
if (context.interimStateValues[this[0]] !== undefined) {
return context.convertBackStringifiedValues(this[0], context.interimStateValues[this[0]]);
}
return context.convertBackStringifiedValues(this[0], states[this[0]]);
}
};
result.getStateAsync = async function() {
if (adapter.config.subscribe) {
const state = await adapter.getForeignStateAsync(this[0]);
return context.convertBackStringifiedValues(this[0], state);
} else {
if (!this[0]) {
return null;
}
if (context.interimStateValues[this[0]] !== undefined) {
return context.convertBackStringifiedValues(this[0], context.interimStateValues[this[0]]);
}
return context.convertBackStringifiedValues(this[0], states[this[0]]);
}
};
result.setState = function (state, isAck, callback) {
if (typeof isAck === 'function') {
callback = isAck;
isAck = undefined;
}
result.setStateAsync(state, isAck)
.then(() =>
typeof callback === 'function' && callback());
return this;
};
result.setStateAsync = async function (state, isAck) {
for (let i = 0; i < this.length; i++) {
await sandbox.setStateAsync(this[i], state, isAck);
}
};
result.setStateChanged = function (state, isAck, callback) {
if (typeof isAck === 'function') {
callback = isAck;
isAck = undefined;
}
result.setStateChangedAsync(state, isAck)
.then(() =>
typeof callback === 'function' && callback());
return this;
};
result.setStateChangedAsync = async function (state, isAck) {
for (let i = 0; i < this.length; i++) {
await sandbox.setStateChangedAsync(this[i], state, isAck);
}
};
result.setStateDelayed = function (state, isAck, delay, clearRunning, callback) {
if (typeof isAck !== 'boolean') {
callback = clearRunning;
clearRunning = delay;
delay = isAck;
isAck = false;
}
if (typeof delay !== 'number') {
callback = clearRunning;
clearRunning = delay;
delay = 0;
}
if (typeof clearRunning !== 'boolean') {
callback = clearRunning;
clearRunning = true;
}
let count = this.length;
for (let i = 0; i < this.length; i++) {
sandbox.setStateDelayed(this[i], state, isAck, delay, clearRunning, () => {
if (!--count && typeof callback === 'function') {
callback();
}
});
}
return this;
};
result.on = function (callbackOrId, value) {
for (let i = 0; i < this.length; i++) {
sandbox.subscribe(this[i], callbackOrId, value);
}
return this;
};
return result;
},
log: function (msg, severity) {
severity = severity || 'info';
// disable log in log handler
if (sandbox.logHandler === severity || sandbox.logHandler === '*') {
return;
}
if (!adapter.log[severity]) {
msg = `Unknown severity level "${severity}" by log of [${msg}]`;
severity = 'warn';
}
if (msg && typeof msg !== 'string') {
msg = mods.util.format(msg);
}
if (debugMode) {
console.log(`${severity}$$${name}$$${msg}`, Date.now());
} else {
adapter.log[severity](`${name}: ${msg}`);
}
},
/**
* @param {string} severity
* @param {function} callback
* @returns {number}
*/
onLog: function (severity, callback) {
if (severity !== 'info' && severity !== 'error' && severity !== 'debug'&& severity !== 'silly' && severity !== 'warn' && severity !== '*') {
sandbox.log(`Unknown severity "${severity}"`, 'warn');
return 0;
}
if (typeof callback !== 'function') {
sandbox.log(`Invalid callback for onLog`, 'warn');
return 0;
}
const handler = {id: Date.now() + Math.floor(Math.random() * 10000), cb: callback, sandbox, severity};
context.logSubscriptions[sandbox.scriptName] = context.logSubscriptions[sandbox.scriptName] || [];
context.logSubscriptions[sandbox.scriptName].push(handler);
context.updateLogSubscriptions();
sandbox.__engine.__subscriptionsLog += 1;
if (sandbox.__engine.__subscriptionsLog % adapter.config.maxTriggersPerScript === 0) {
sandbox.log(`More than ${sandbox.__engine.__subscriptionsLog} log subscriptions registered. Check your script!`, 'warn');
}
},
onLogUnregister: function (idOrCallbackOrSeverity) {
let found = false;
if (context.logSubscriptions[sandbox.scriptName]) {
for (let i = 0; i < context.logSubscriptions[sandbox.scriptName].length ; i++) {
if (context.logSubscriptions[sandbox.scriptName][i].cb === idOrCallbackOrSeverity ||
context.logSubscriptions[sandbox.scriptName][i].id === idOrCallbackOrSeverity ||
context.logSubscriptions[sandbox.scriptName][i].severity === idOrCallbackOrSeverity) {
context.logSubscriptions[sandbox.scriptName].splice(i, 1);
if (!context.logSubscriptions[sandbox.scriptName].length) {
delete context.logSubscriptions[sandbox.scriptName];
}
found = true;
// if not deletion via ID
if (typeof idOrCallbackOrSeverity === 'number') {
break;
}
}
}
}
if (found) {
context.updateLogSubscriptions();
sandbox.__engine.__subscriptionsLog--;
}
return found;
},
exec: function (cmd, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (!adapter.config.enableExec) {
const error = 'exec is not available. Please enable "Enable Exec" option in instance settings';
sandbox.log(error, 'error');
if (typeof callback === 'function') {
setImmediate(callback, error);
}
} else {
sandbox.verbose && sandbox.log(`exec(cmd=${cmd})`, 'info');
if (debug) {
sandbox.log(words._('Command %s was not executed, while debug mode is active', cmd), 'warn');
if (typeof callback === 'function') {
setImmediate(function () {
callback();
});
}
} else {
return mods.child_process.exec(cmd, options, (error, stdout, stderr) => {
if (typeof callback === 'function') {
try {
callback.call(sandbox, error, stdout, stderr);
} catch (e) {
errorInCallback(e);
}
}
});
}
}
},
email: function (msg) {
sandbox.verbose && sandbox.log(`email(msg=${JSON.stringify(msg)})`, 'info');
sandbox.log(`email(msg=${JSON.stringify(msg)}) is deprecated. Please use sendTo instead!`, 'warn');
adapter.sendTo('email', msg);
},
pushover: function (msg) {
sandbox.verbose && sandbox.log(`pushover(msg=${JSON.stringify(msg)})`, 'info');
sandbox.log(`pushover(msg=${JSON.stringify(msg)}) is deprecated. Please use sendTo instead!`, 'warn');
adapter.sendTo('pushover', msg);
},
httpGet: function(url, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
const config = {
...getHttpRequestConfig(url, options),
method: 'get',
};
sandbox.verbose && sandbox.log(`httpGet(config=${JSON.stringify(config)})`, 'info');
mods.axios.default(config)
.then(respo