UNPKG

iobroker.javascript

Version:
1,175 lines (1,076 loc) 179 kB
/// <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