iobroker.javascript
Version:
Javascript/Coffescript Script Engine for ioBroker
1,128 lines (1,039 loc) • 113 kB
JavaScript
/// <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);
}