UNPKG

iobroker.javascript

Version:

Javascript/Coffescript Script Engine for ioBroker

1,128 lines (1,039 loc) 113 kB
/// <reference path="./javascript.d.ts" /> /* jslint global: console */ /* eslint-env node */ 'use strict'; const isObject = require('./tools').isObject; const isArray = require('./tools').isArray; // let context = { // adapter, // mods, // errorLogFunction, // subscriptions, // subscribedPatterns, // states, // adapterSubs, // objects, // cacheObjectEnums, // stateIds, // logWithLineInfo, // timers, // enums, // channels, // devices, // isEnums // }; /** * @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 nodeSchedule = require('node-schedule'); /** @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; function errorInCallback(e) { adapter.setState('scriptProblem.' + name.substring('script.js.'.length), true, true); context.logError('Error in callback', e); } 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 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.indexOf('*') === -1) { adapter.getForeignState(pattern, function (err, state) { if (state) states[pattern] = state; }); } else { adapter.getForeignStates(pattern, function (err, _states) { if (_states) { for (const id in _states) { if (!_states.hasOwnProperty(id)) continue; states[id] = _states[id]; } } }); } } else { context.subscribedPatterns[pattern]++; } } } /** * @typedef PatternCompareFunctionArray * @type {Array<any> & {logic?: string}} */ function getPatternCompareFunctions(pattern) { let func; /** @type {PatternCompareFunctionArray} */ const functions = []; functions.logic = pattern.logic || 'and'; //adapter.log.info('## '+JSON.stringify(pattern)); for (const key in pattern) { if (!pattern.hasOwnProperty(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) { if (str[0] === '*') { // wildcard at the start, match the end of the string return new RegExp(str.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); } else if (str[str.length - 1] === '*') { // wildcard at the end, match the start of the string return new RegExp('^' + str.replace(/\./g, '\\.').replace(/\*/g, '.*')); } else { // wildcard potentially in the middle, match whatever return new RegExp(str.replace(/\./g, '\\.').replace(/\*/g, '.*')); } } /** * 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" aswell * 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 ; } /** * Tests if the given value has the correct type as defined in common.type * @param {iobJS.CommonType} commonType * @param {any} value */ function checkCommonType(commonType, value) { return commonType === 'array' ? isArray(value) : (commonType === 'object' ? isObject(value) : commonType === typeof value); } function setStateHelper(sandbox, isBinary, id, state, isAck, callback) { const setStateFunc = isBinary ? adapter.setBinaryState.bind(adapter) : adapter.setForeignState.bind(adapter); if (typeof isAck === 'function') { callback = isAck; isAck = undefined; } if (!isBinary) { 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 type of state if (!objects[id] && objects[adapter.namespace + '.' + id]) { id = adapter.namespace + '.' + id; } const common = objects[id] ? objects[id].common : null; if (common && common.type && common.type !== 'mixed' && common.type !== 'file' && common.type !== 'json') { if (state && isObject(state) && state.val !== undefined) { if (!checkCommonType(common.type, state.val)) { context.logWithLineInfo && context.logWithLineInfo.warn('Wrong type of ' + id + ': "' + typeof state.val + '". Please fix, while deprecated and will not work in next versions.'); //return; } } else { if (!checkCommonType(common.type, state)) { context.logWithLineInfo && context.logWithLineInfo.warn('Wrong type of ' + id + ': "' + typeof state + '". Please fix, while deprecated and will not work in next versions.'); //return; } } } // Check min and max of value if (!isBinary && 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 (!isBinary && common && typeof state === 'number') { if (common.min !== undefined && state < common.min) state = common.min; if (common.max !== undefined && state > common.max) state = common.max; } if (objects[id]) { 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') { setTimeout(() => { try { callback.call(sandbox); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } }, 0); } } else { setStateFunc(id, state, err => { err && sandbox.log('setForeignState: ' + err, 'error'); if (typeof callback === 'function') { try { callback.call(sandbox); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } } }); } } else if (objects[adapter.namespace + '.' + id]) { sandbox.verbose && sandbox.log('setState(id=' + id + ', state=' + JSON.stringify(state) + ')', 'info'); if (debug) { sandbox.log('setState(' + id + ', ' + JSON.stringify(state) + ') - ' + words._('was not executed, while debug mode is active'), 'warn'); if (typeof callback === 'function') { setTimeout(() => { try { callback.call(sandbox); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } }, 0); } } else { setStateFunc(adapter.namespace + '.' + id, state, err => { err && sandbox.log('setState: ' + err, 'error'); if (typeof callback === 'function') { try { callback.call(sandbox); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } } }); } } else { if (objects[id]) { if (objects[id].type === 'state') { 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') { setTimeout(() => { try { callback.call(sandbox); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } }, 0); } } else { setStateFunc(id, state, err => { err && sandbox.log('setForeignState: ' + err, 'error'); if (typeof callback === 'function') { try { callback.call(sandbox); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } } }); } } else { adapter.log.warn('Cannot set value of non-state object "' + id + '"'); if (typeof callback === 'function') { try { callback.call(sandbox, 'Cannot set value of non-state object "' + id + '"'); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } } } } else if (objects[adapter.namespace + '.' + id]) { if (objects[adapter.namespace + '.' + id].type === 'state') { sandbox.verbose && sandbox.log(`setState(id=${adapter.namespace}.${id}, state=${JSON.stringify(state)})`, 'info'); if (debug) { sandbox.log(`setState(id=${adapter.namespace}.${id}, state=${JSON.stringify(state)}) - ${words._('was not executed, while debug mode is active')}`, 'warn'); if (typeof callback === 'function') { setTimeout(() => { try { callback.call(sandbox); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } }, 0); } } else { setStateFunc(adapter.namespace + '.' + id, state, err => { err && sandbox.log('setState: ' + err, 'error'); if (typeof callback === 'function') { try { callback.call(sandbox); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } } }); } } else { adapter.log.warn('Cannot set value of non-state object "' + adapter.namespace + '.' + id + '"'); if (typeof callback === 'function') { try { callback.call(sandbox, 'Cannot set value of non-state object "' + adapter.namespace + '.' + id + '"'); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } } } } else { context.logWithLineInfo && context.logWithLineInfo.warn('State "' + id + '" not found'); if (typeof callback === 'function') { try { callback.call(sandbox, 'State "' + id + '" not found'); } catch (e) { errorInCallback(e); //adapter.log.error('Error in callback: ' + e) } } } } } const sandbox = { mods: mods, _id: script._id, name: name, // deprecated scriptName: name, instance: adapter.instance, verbose: verbose, request: mods.request, exports: {}, // Polyfill for the exports object in TypeScript modules require: function (md) { console.log('REQUIRE: ' + md); if (mods[md]) { return mods[md]; } else { try { mods[md] = require(__dirname + '/../node_modules/' + md); return mods[md]; } catch (e) { try { mods[md] = require(__dirname + '/../../' + md); return mods[md]; } catch (e) { adapter.setState('scriptProblem.' + name.substring('script.js.'.length), true, true); context.logError(name, e, 6); } } } }, Buffer: Buffer, __engine: { __subscriptions: 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.setBinaryState = function () { return this; }; result.on = function () { return this; }; } if (isInsideEnumString) { adapter.log.warn('Invalid selector: enum close bracket ")" cannot be found in "' + selector + '"'); result.error = 'Invalid selector: enum close bracket ")" cannot be found'; return result; } else if (isInsideCommonString) { adapter.log.warn('Invalid selector: common close bracket "]" cannot be found in "' + selector + '"'); result.error = 'Invalid selector: common close bracket "]" cannot be found'; return result; } else if (isInsideNativeString) { adapter.log.warn('Invalid selector: native close bracket "}" cannot be found in "' + selector + '"'); 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}`; adapter.log.warn(message); 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 afterwards if (!context.channels || !context.devices) { context.channels = {}; context.devices = {}; for (const _id in objects) { if (objects.hasOwnProperty(_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); } } } } /** * 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 => { return selector.idRegExp == null || 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 == null || obj.common == null) return false; const objCommon = obj.common; // make sure this object satisfies all selectors return commonSelectors.every(selector => { return ( // 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 == null || obj.native == null) return false; const objNative = obj.native; // make sure this object satisfies all selectors return nativeSelectors.every(selector => { return ( // 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.indexOf(_enum) > -1); } /** @type {string[]} */ let res = []; if (name === 'channel') { // go through all channels res = Object.keys(context.channels) // filter out those that don't match every ID selector for the channel ID .filter(channelId => applyIDSelectors(channelId, objectIdSelectors)) // filter out those that don't match every common selector .filter(channelId => applyCommonSelectors(channelId)) // filter out those that don't match every native selector .filter(channelId => applyNativeSelectors(channelId)) // filter out those that don't match every enum selector .filter(channelId => applyEnumSelectors(channelId)) // retrieve the state ID collection for all remaining channels .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 .filter(stateId => applyIDSelectors(stateId, stateIdSelectors)) ; } else if (name === 'device') { // go through all devices res = Object.keys(context.devices) // filter out those that don't match every ID selector for the channel ID .filter(deviceId => applyIDSelectors(deviceId, objectIdSelectors)) // filter out those that don't match every common selector .filter(deviceId => applyCommonSelectors(deviceId)) // filter out those that don't match every native selector .filter(deviceId => applyNativeSelectors(deviceId)) // filter out those that don't match every enum selector .filter(deviceId => applyEnumSelectors(deviceId)) // retrieve the state ID collection for all remaining devices .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 .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 aswell if (name && name !== 'state') { const r = new RegExp('^' + name.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); res = res.filter(id => r.test(id)); } res = res // filter out those that don't match every ID selector for the object ID or the state ID .filter(id => applyIDSelectors(id, objectIdSelectors)) .filter(id => applyIDSelectors(id, stateIdSelectors)) // filter out those that don't match every common selector .filter(id => applyCommonSelectors(id)) // filter out those that don't match every native selector .filter(id => applyNativeSelectors(id)) // filter out those that don't match every enum selector .filter(id => applyEnumSelectors(id)) ; } for (let i = 0; i < res.length; i++) { result[i] = res[i]; } result.length = res.length; 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], callback); } } else { return this[0] ? states[this[0]] : null; } }; result.getBinaryState = function (callback) { if (adapter.config.subscribe) { if (typeof callback !== 'function') { sandbox.log('You cannot use this function synchronous', 'error'); } else { adapter.getBinaryState(this[0], callback); } } else { return this[0] ? states[this[0]] : null; } }; result.setState = function (state, isAck, callback) { if (typeof isAck === 'function') { callback = isAck; isAck = undefined; } if (isAck === true || isAck === false || isAck === 'true' || isAck === 'false') { if (isObject(state)) { state.ack = isAck; } else { state = {val: state, ack: isAck}; } } let cnt = 0; for (let i = 0; i < this.length; i++) { cnt++; adapter.setForeignState(this[i], state, function () { if (!--cnt && typeof callback === 'function') callback(); }); } return this; }; result.setBinaryState = function (state, isAck, callback) { if (typeof isAck === 'function') { callback = isAck; isAck = undefined; } if (isAck === true || isAck === false || isAck === 'true' || isAck === 'false') { if (isObject(state)) { state.ack = isAck; } else { state = {val: state, ack: isAck}; } } let cnt = 0; for (let i = 0; i < this.length; i++) { cnt++; adapter.setBinaryState(this[i], state, function () { if (!--cnt && 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, sev) { if (!sev) sev = 'info'; if (!adapter.log[sev]) { msg = 'Unknown severity level "' + sev + '" by log of [' + msg + ']'; sev = 'warn'; } adapter.log[sev](name + ': ' + msg); }, exec: function (cmd, callback) { if (!adapter.config.enableExec) { const error = 'exec is not available. Please enable "Enable Exec" option in instance settings'; adapter.log.error(error); sandbox.log(error); if (typeof callback === 'function') { setImmediate(callback, error); } } else { if (sandbox.verbose) { sandbox.log('exec: ' + 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, callback); } } }, email: function (msg) { sandbox.verbose && sandbox.log('email(msg=' + JSON.stringify(msg) + ')', 'info'); adapter.sendTo('email', msg); }, pushover: function (msg) { sandbox.verbose && sandbox.log('pushover(msg=' + JSON.stringify(msg) + ')', 'info'); adapter.sendTo('pushover', msg); }, subscribe: function (pattern, callbackOrId, value) { if ((typeof pattern === 'string' && pattern[0] === '{') || (typeof pattern === 'object' && pattern.period)) { return sandbox.schedule(pattern, callbackOrId); } else if (pattern && Array.isArray(pattern)) { const result = []; for (let t = 0; t < pattern.length; t++) { result.push(sandbox.subscribe(pattern[t], callbackOrId, value)); } return result; } if (pattern && pattern.id && Array.isArray(pattern.id)) { const result_ = []; for (let tt = 0; tt < pattern.id.length; tt++) { const pa = JSON.parse(JSON.stringify(pattern)); pa.id = pattern.id[tt]; result_.push(sandbox.subscribe(pa, callbackOrId, value)); } return result_; } // try to detect astro or cron (by spaces) if (isObject(pattern) || (typeof pattern === 'string' && pattern.match(/[,/\d*]+\s[,/\d*]+\s[,/\d*]+/))) { if (pattern.astro) { return sandbox.schedule(pattern, callbackOrId); } else if (pattern.time) { return sandbox.schedule(pattern.time, callbackOrId); } } let callback; sandbox.__engine.__subscriptions += 1; // source is set by regexp if defined as /regexp/ if (!isObject(pattern) || pattern instanceof RegExp || pattern.source) { pattern = {id: pattern, change: 'ne'}; } if (pattern.id !== undefined && !pattern.id) { adapter.log.error('Error by subscription: empty ID defined. All states matched.'); return; } if (pattern.q === undefined) { pattern.q = 0; } // add adapter namespace if nothing given if (pattern.id && typeof pattern.id === 'string' && pattern.id.indexOf('.') === -1) { pattern.id = adapter.namespace + '.' + pattern.id; } if (typeof callbackOrId === 'function') { callback = callbackOrId; } else { if (typeof value === 'undefined') { callback = function (obj) { sandbox.setState(callbackOrId, obj.newState.val); }; } else { callback = function (/* obj */) { sandbox.setState(callbackOrId, value); }; } } const subs = { pattern: pattern, callback: obj => { if (typeof callback === 'function') { try { callback.call(sandbox, obj); } catch (e) { errorInCallback(e); // adapter.log.error('Error in callback: ' + e); } } }, name: name }; // try to extract adapter if (pattern.id && typeof pattern.id === 'string') { const parts = pattern.id.split('.'); const a = parts[0] + '.' + parts[1]; const _adapter = 'system.adapter.' + a; if (objects[_adapter] && objects[_adapter].common && objects[_adapter].common.subscribable) { const alive = 'system.adapter.' + a + '.alive'; context.adapterSubs[alive] = context.adapterSubs[alive] || []; const subExists = context.adapterSubs[alive].filter(sub => { return sub === pattern.id; }).length > 0; if (!subExists) { context.adapterSubs[alive].push(pattern.id); adapter.sendTo(a, 'subscribe', pattern.id); } } } sandbox.verbose && sandbox.log('subscribe: ' + JSON.stringify(subs), 'info'); subscribePattern(script, pattern.id); subs.patternCompareFunctions = getPatternCompareFunctions(pattern); context.subscriptions.push(subs); if (pattern.enumName || pattern.enumId) context.isEnums = true; return subs; }, getSubscriptions: function () { const result = {}; for (let s = 0; s < context.subscriptions.length; s++) { result[context.subscriptions[s].pattern.id] = result[context.subscriptions[s].pattern.id] || []; result[context.subscriptions[s].pattern.id].push({ name: context.subscriptions[s].name, pattern: context.subscriptions[s].pattern }); } sandbox.verbose && sandbox.log('getSubscriptions() => ' + JSON.stringify(result), 'info'); return result; }, adapterSubscribe: function (id) { if (typeof id !== 'string') { adapter.log.error('adapterSubscribe: invalid type of id' + typeof id); return; } const parts = id.split('.'); const _adapter = 'system.adapter.' + parts[0] + '.' + parts[1]; if (objects[_adapter] && objects[_adapter].common && objects[_adapter].common.subscribable) { const a = parts[0] + '.' + parts[1]; const alive = 'system.adapter.' + a + '.alive'; context.adapterSubs[alive] = context.adapterSubs[alive] || []; context.adapterSubs[alive].push(id); sandbox.verbose && sandbox.log('adapterSubscribe: ' + a + ' - ' + id, 'info'); adapter.sendTo(a, 'subscribe', id); } }, adapterUnsubscribe: function (id) { return sandbox.unsubscribe(id); }, unsubscribe: function (idOrObject) { if (idOrObject && Array.isArray(idOrObject)) { const result = []; for (let t = 0; t < idOrObject.length; t++) { result.push(sandbox.unsubscribe(idOrObject[t])); } return result; } sandbox.verbose && sandbox.log('adapterUnsubscribe(id=' + idOrObject + ')', 'info'); if (isObject(idOrObject)) { for (let i = context.subscriptions.length - 1; i >= 0; i--) { if (context.subscriptions[i] === idOrObject) { unsubscribePattern(context.subscriptions[i].pattern.id); context.subscriptions.splice(i, 1); sandbox.__engine.__subscriptions--; return true; } } } else { let deleted = 0; for (let i = context.subscriptions.length - 1; i >= 0; i--) { if (context.subscriptions[i].name === name && context.subscriptions[i].pattern.id === idOrObject) { deleted++; unsubscribePattern(context.subscriptions[i].pattern.id); context.subscriptions.splice(i, 1); sandbox.__engine.__subscriptions--; } } return !!deleted; } }, on: function (pattern, callbackOrId, value) { return sandbox.subscribe(pattern, callbackOrId, value); }, /** Registers a one-time subscription which automatically unsubscribes after the first invocation */ once: function (pattern, callback) { function _once(cb) { /** @type {iobJS.StateChangeHandler} */ const handler = (obj) => { sandbox.unsubscribe(subscription); typeof cb === 'function' && cb(obj); }; const subscription = sandbox.subscribe(pattern, handler); return subscription; } if (typeof callback === 'function') { // Callback-style: once("id", (obj) => { ... }) return _once(callback); } else { // Promise-style: once("id").then(obj => { ... }) return new Promise(resolve => _once(resolve)); } }, schedule: function (pattern, callback) { if (typeof callback !== 'function') { adapter.log.error(name + ': schedule callback missing'); return; } if ((typeof pattern === 'string' && pattern[0] === '{') || (typeof pattern === 'object' && pattern.period)) { sandbox.verbose && sandbox.log('schedule(wizard=' + (typeof pattern === 'object' ? JSON.stringify(pattern) : pattern) + ')', 'info'); const schedule = context.scheduler.add(pattern, callback); schedule && script.wizards.push(schedule); return schedule; } sandbox.__engine.__schedules += 1; if (typeof pattern === 'object' && pattern.astro) { const nowdate = new Date(); if (adapter.config.latitude === undefined || adapter.config.longitude === undefined || adapter.config.latitude === '' || adapter.config.longitude === '' || adapter.config.latitude === null || adapter.config.longitude === null) { adapter.log.error('Longitude or latitude does not set. Cannot use astro.'); return; } let ts = mods.suncalc.getTimes(nowdate, adapter.config.latitude, adapter.config.longitude)[pattern.astro]; if (ts.getTime().toString() === 'NaN') { adapter.log.warn('Cannot calculate "' + pattern.astro + '" for ' + adapter.config.latitude + ', ' + adapter.config.longitude); ts = new Date(nowdate.getTime()); if (pattern.astro === 'sunriseEnd' || pattern.astro === 'goldenHourEnd' || pattern.astro === 'sunset' || pattern.astro === 'nightEnd' || pattern.astro === 'nauticalDusk') { ts.setMinutes(59); ts.setHours(23); ts.setSeconds(59); } else { ts.setMinutes(59); ts.setHours(23); ts.setSeconds(58); }