iobroker.javascript
Version:
Rules Engine for ioBroker
1,128 lines (1,015 loc) • 108 kB
JavaScript
/*
* Javascript adapter
*
* The MIT License (MIT)
*
* Copyright (c) 2014-2024 bluefox <dogafox@gmail.com>,
*
* Copyright (c) 2014 hobbyquaker
*/
/* jshint -W097 */
/* jshint -W083 */
/* jshint strict: false */
/* jslint node: true */
/* jshint shadow: true */
'use strict';
const vm = require('node:vm');
const nodeFS = require('node:fs');
const nodePath = require('node:path');
const tsc = require('virtual-tsc');
const Mirror = require('./lib/mirror');
const fork = require('child_process').fork;
const mods = {
fs: {},
dgram: require('node:dgram'),
crypto: require('node:crypto'),
dns: require('node:dns'),
events: require('node:events'),
http: require('node:http'),
https: require('node:https'),
http2: require('node:http2'),
net: require('node:net'),
os: require('node:os'),
path: require('node:path'),
util: require('node:util'),
child_process: require('node:child_process'),
stream: require('node:stream'),
zlib: require('node:zlib'),
suncalc: require('suncalc2'),
axios: require('axios'),
wake_on_lan: require('wake_on_lan'),
nodeSchedule: require('node-schedule')
};
/**
* List of forbidden Locations for a mirror directory
* relative to the default data directory
* ATTENTION: the same list is also located in index_m.html!!
* @type {*[]}
*/
const forbiddenMirrorLocations = [
'backup-objects',
'files',
'backitup',
'../backups',
'../node_modules',
'../log'
];
const utils = require('@iobroker/adapter-core');
const words = require('./lib/words');
const sandBox = require('./lib/sandbox');
const eventObj = require('./lib/eventObj');
const Scheduler = require('./lib/scheduler');
const { targetTsLib, tsCompilerOptions, jsDeclarationCompilerOptions } = require('./lib/typescriptSettings');
const { hashSource, isObject } = require('./lib/tools');
const { isDeepStrictEqual } = require('node:util');
const {
resolveTypescriptLibs,
resolveTypings,
scriptIdToTSFilename,
transformScriptBeforeCompilation,
transformGlobalDeclarations
} = require('./lib/typescriptTools');
const packageJson = require('./package.json');
const SCRIPT_CODE_MARKER = 'script.js.';
const stopCounters = {};
let setStateCountCheckInterval = null;
let webstormDebug;
let debugMode;
if (process.argv) {
for (let a = 1; a < process.argv.length; a++) {
if (process.argv[a].startsWith('--webstorm')) {
webstormDebug = process.argv[a].replace(/^(.*?=\s*)/, '');
}
if (process.argv[a] === '--debugScript') {
if (!process.argv[a + 1]) {
console.log('No script name provided');
process.exit(300);
} else {
debugMode = process.argv[a + 1];
}
}
}
}
const isCI = !!process.env.CI;
// ambient declarations for typescript
/** @type {Record<string, string>} */
let tsAmbient;
// TypeScript's scripts are only recompiled if their source hash changes. If an adapter update fixes compilation bugs,
// a user won't notice until he changes and re-saves the script. To avoid that, we also include the
// adapter version and TypeScript version in the hash
const tsSourceHashBase = `versions:adapter=${packageJson.version},typescript=${packageJson.dependencies.typescript}`;
let mirror;
/** @type {boolean} if logs are subscribed or not */
let logSubscribed;
/**
* @param {string} scriptID - The current script the declarations were generated from
* @param {string} declarations
*/
function provideDeclarationsForGlobalScript(scriptID, declarations) {
// Remember which declarations this global script had access to,
// we need this so the editor doesn't show a duplicate identifier error
if (globalDeclarations != null && globalDeclarations !== '') {
knownGlobalDeclarationsByScript[scriptID] = globalDeclarations;
}
// and concatenate the global declarations for the next scripts
globalDeclarations += declarations + '\n';
// remember all previously generated global declarations,
// so global scripts can reference each other
const globalDeclarationPath = 'global.d.ts';
tsAmbient[globalDeclarationPath] = globalDeclarations;
// make sure the next script compilation has access to the updated declarations
tsServer.provideAmbientDeclarations({
[globalDeclarationPath]: globalDeclarations
});
jsDeclarationServer.provideAmbientDeclarations({
[globalDeclarationPath]: globalDeclarations
});
}
// taken from here: https://stackoverflow.com/questions/11887934/how-to-check-if-dst-daylight-saving-time-is-in-effect-and-if-so-the-offset
function dstOffsetAtDate(dateInput) {
const fullYear = dateInput.getFullYear() | 0;
// "Leap Years are any year that can be exactly divided by 4 (2012, 2016, etc)
// except if it can be exactly divided by 100, then it isn't (2100, 2200, etc)
// except if it can be exactly divided by 400, then it is (2000, 2400)"
// (https://www.mathsisfun.com/leap-years.html).
const isLeapYear = ((fullYear & 3) | (fullYear/100 & 3)) === 0 ? 1 : 0;
// (fullYear & 3) = (fullYear % 4), but faster
//Alternative:var isLeapYear=(new Date(currentYear,1,29,12)).getDate()===29?1:0
const fullMonth = dateInput.getMonth() | 0;
return (
// 1. We know what the time since the Epoch really is
(+dateInput) // same as the dateInput.getTime() method
// 2. We know what the time since the Epoch at the start of the year is
- (+new Date(fullYear, 0)) // day defaults to 1 if not explicitly zeroed
// 3. Now, subtract what we would expect the time to be if daylight savings
// did not exist. This yields the time-offset due to daylight savings.
- ((
((
// Calculate the day of the year in the Gregorian calendar
// The code below works based upon the facts of signed right shifts
// • (x) >> n: shifts n and fills in the n highest bits with 0s
// • (-x) >> n: shifts n and fills in the n highest bits with 1s
// (This assumes that x is a positive integer)
-1 + // first day in the year is day 1
(31 & ((-fullMonth) >> 4)) + // January // (-11)>>4 = -1
((28 + isLeapYear) & ((1 - fullMonth) >> 4)) + // February
(31 & ((2 - fullMonth) >> 4)) + // March
(30 & ((3 - fullMonth) >> 4)) + // April
(31 & ((4 - fullMonth) >> 4)) + // May
(30 & ((5 - fullMonth) >> 4)) + // June
(31 & ((6 - fullMonth) >> 4)) + // July
(31 & ((7 - fullMonth) >> 4)) + // August
(30 & ((8 - fullMonth) >> 4)) + // September
(31 & ((9 - fullMonth) >> 4)) + // October
(30 & ((10 - fullMonth) >> 4)) + // November
// There are no months past December: the year rolls into the next.
// Thus, fullMonth is 0-based, so it will never be 12 in Javascript
(dateInput.getDate() | 0) // get day of the month
) & 0xffff) * 24 * 60 // 24 hours in a day, 60 minutes in an hour
+ (dateInput.getHours() & 0xff) * 60 // 60 minutes in an hour
+ (dateInput.getMinutes() & 0xff)
)|0) * 60 * 1000 // 60 seconds in a minute * 1000 milliseconds in a second
- (dateInput.getSeconds() & 0xff) * 1000 // 1000 milliseconds in a second
- dateInput.getMilliseconds()
);
}
function loadTypeScriptDeclarations() {
// try to load the typings on disk for all 3rd party modules
const packages = [
'node', // this provides auto-completion for most builtins
'request', // preloaded by the adapter
];
// Also include user-selected libraries (but only those that are also installed)
if (
adapter.config
&& typeof adapter.config.libraries === 'string'
&& typeof adapter.config.libraryTypings === 'string'
) {
const installedLibs = adapter.config.libraries
.split(/[,;\s]+/)
.map((s) => s.trim().split('@')[0])
.filter((s) => !!s);
const wantsTypings = adapter.config.libraryTypings.split(/[,;\s]+/).map(s => s.trim()).filter(s => !!s);
// Add all installed libraries the user has requested typings for to the list of packages
for (const lib of installedLibs) {
if (wantsTypings.includes(lib) && !packages.includes(lib)) {
packages.push(lib);
}
}
// Some packages have sub-modules (e.g. rxjs/operators) that are not exposed through the main entry point
// If typings are requested for them, also add them if the base module is installed
for (const lib of wantsTypings) {
// Extract the package name and check if we need to add it
if (!lib.includes('/')) {
continue;
}
const pkgName = lib.substr(0, lib.indexOf('/'));
if (installedLibs.includes(pkgName) && !packages.includes(lib)) {
packages.push(lib);
}
}
}
for (const pkg of packages) {
let pkgTypings = resolveTypings(
pkg,
// node needs ambient typings, so we don't wrap it in declare module
pkg !== 'node'
);
if (!pkgTypings) {
// Create empty dummy declarations so users don't get the "not found" error
// for installed packages
pkgTypings = {
[`node_modules/@types/${pkg}/index.d.ts`]: `declare module "${pkg}";`,
};
}
adapter.log.debug(`Loaded TypeScript definitions for ${pkg}: ${JSON.stringify(Object.keys(pkgTypings))}`);
// remember the declarations for the editor
Object.assign(tsAmbient, pkgTypings);
// and give the language servers access to them
tsServer.provideAmbientDeclarations(pkgTypings);
jsDeclarationServer.provideAmbientDeclarations(pkgTypings);
}
}
const context = {
mods,
objects: {},
states: {},
interimStateValues: {},
stateIds: [],
errorLogFunction: null,
subscriptions: [],
subscriptionsFile: [],
subscriptionsObject: [],
subscribedPatterns: {},
subscribedPatternsFile: {},
adapterSubs: {},
cacheObjectEnums: {},
isEnums: false, // If some subscription wants enum
channels: null,
devices: null,
logWithLineInfo: null,
scheduler: null,
timers: {},
enums: [],
timerId: 0,
names: {},
scripts: {},
messageBusHandlers: {},
logSubscriptions: {},
folderCreationVerifiedObjects: {},
updateLogSubscriptions,
convertBackStringifiedValues,
updateObjectContext,
prepareStateObject,
debugMode,
timeSettings: {
format12: false,
leadingZeros: true
},
rulesOpened: null, //opened rules
getAbsoluteDefaultDataDir: utils.getAbsoluteDefaultDataDir,
};
const regExGlobalOld = /_global$/;
const regExGlobalNew = /script\.js\.global\./;
function checkIsGlobal(obj) {
return obj && obj.common && (regExGlobalOld.test(obj.common.name) || regExGlobalNew.test(obj._id));
}
function convertBackStringifiedValues(id, state) {
if (state && typeof state.val === 'string' &&
context.objects[id] && context.objects[id].common &&
(context.objects[id].common.type === 'array' || context.objects[id].common.type === 'object')) {
try {
state.val = JSON.parse(state.val);
} catch (err) {
if (id.startsWith('javascript.') || id.startsWith('0_userdata.0')) {
adapter.log.info(`Could not parse value for id ${id} into ${context.objects[id].common.type}: ${err.message}`);
} else {
adapter.log.debug(`Could not parse value for id ${id} into ${context.objects[id].common.type}: ${err.message}`);
}
}
}
return state;
}
function prepareStateObject(id, state, isAck) {
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};
}
}
if (adapter.config.subscribe) {
return state;
}
// set other values to have a full state object
// mirrors logic from statesInRedis
if (state.ts === undefined) {
state.ts = Date.now();
}
if (state.q === undefined) {
state.q = 0;
}
state.from =
typeof state.from === 'string' && state.from !== '' ? state.from : `system.adapter.${adapter.namespace}`;
if (state.lc === undefined) {
const formerStateValue = context.interimStateValues[id] || context.states[id];
if (!formerStateValue) {
state.lc = state.ts;
} else {
// isDeepStrictEqual works on objects and primitive values
const hasChanged = !isDeepStrictEqual(formerStateValue.val, state.val);
if (!formerStateValue.lc || hasChanged) {
state.lc = state.ts;
} else {
state.lc = formerStateValue.lc;
}
}
}
return state;
}
function fileMatching(sub, id, fileName) {
if (sub.idRegEx) {
if (!sub.idRegEx.test(id)) {
return false;
}
} else {
if (sub.id !== id) {
return false;
}
}
if (sub.fileRegEx) {
if (!sub.fileRegEx.test(fileName)) {
return false;
}
} else {
if (sub.fileNamePattern !== fileName) {
return false;
}
}
return true;
}
/**
* @type {Set<string>}
* Stores the IDs of script objects whose change should be ignored because
* the compiled source was just updated
*/
const ignoreObjectChange = new Set();
let objectsInitDone = false;
let statesInitDone = false;
/** @type {ioBroker.Adapter} */
let adapter;
function startAdapter(options) {
options = options || {};
Object.assign(options, {
name: 'javascript',
useFormatDate: true, // load float formatting
/**
* @param id { string }
* @param obj { ioBroker.Object }
*/
objectChange: (id, obj) => {
// Check if we should ignore this change (once!) because we just updated the compiled sources
if (ignoreObjectChange.has(id)) {
// Update the cached script object and do nothing more
context.objects[id] = obj;
ignoreObjectChange.delete(id);
return;
}
// When still in initializing: already remember current values,
// but data structures are initialized elsewhere
if (!objectsInitDone) {
if (obj) {
context.objects[id] = obj;
}
return;
}
if (id.startsWith('enum.')) {
// clear cache
context.cacheObjectEnums = {};
// update context.enums array
if (obj) {
// If new
if (!context.enums.includes(id)) {
context.enums.push(id);
context.enums.sort();
}
} else {
const pos = context.enums.indexOf(id);
// if deleted
if (pos !== -1) {
context.enums.splice(pos, 1);
}
}
}
if (obj && id === 'system.config') {
// set language for debug messages
if (obj.common && obj.common.language) {
words.setLanguage(obj.common.language);
}
}
// update stored time format for variables.dayTime
if (id === adapter.namespace + '.variables.dayTime' && obj && obj.native) {
context.timeSettings.format12 = obj.native.format12 || false;
context.timeSettings.leadingZeros = obj.native.leadingZeros === undefined ? true : obj.native.leadingZeros;
}
// send changes to disk mirror
mirror && mirror.onObjectChange(id, obj);
const formerObj = context.objects[id];
updateObjectContext(id, obj); // Update all Meta object data
// for alias object changes on state objects we need to manually update the
// state cache value because new value is only published on next change
if (obj && obj.type === 'state' && id.startsWith('alias.0.')) {
adapter.getForeignState(id, (err, state) => {
if (err) {
return;
}
if (state) {
context.states[id] = state;
} else if (context.states[id] !== undefined) {
delete context.states[id];
}
});
}
context.subscriptionsObject.forEach(sub => {
// ToDo: implement comparing with id.0.* too
if (sub.pattern === id) {
try {
sub.callback(id, obj);
} catch (err) {
adapter.log.error(`Error in callback: ${err}`);
}
}
});
// handle Script object updates
if (!obj && formerObj && formerObj.type === 'script') {
// Object Deleted just now
if (checkIsGlobal(formerObj)) {
// it was a global Script, and it was enabled and is now deleted => restart adapter
if (formerObj.enabled) {
adapter.log.info(`Active global Script ${id} deleted. Restart instance.`);
adapter.restart();
}
} else if (formerObj.common && formerObj.common.engine === `system.adapter.${adapter.namespace}`) {
// It was a non-global Script and deleted => stop and remove it
stop(id);
// delete scriptEnabled.blabla variable
const idActive = 'scriptEnabled.' + id.substring(SCRIPT_CODE_MARKER.length);
adapter.delObject(idActive);
adapter.delState(idActive);
// delete scriptProblem.blabla variable
const idProblem = 'scriptProblem.' + id.substring(SCRIPT_CODE_MARKER.length);
adapter.delObject(idProblem);
adapter.delState(idProblem);
}
} else if (!formerObj && obj && obj.type === 'script') {
// New script that does not exist before
if (checkIsGlobal(obj)) {
// new global script added => restart adapter
if (obj.common.enabled) {
adapter.log.info(`Active global Script ${id} created. Restart instance.`);
adapter.restart();
}
} else if (obj.common && obj.common.engine === `system.adapter.${adapter.namespace}`) {
// new non-global script - create states for scripts
createActiveObject(id, obj.common.enabled, () => createProblemObject(id));
if (obj.common.enabled) {
// if enabled => Start script
load(id);
}
}
} else if (obj && obj.type === 'script' && formerObj && formerObj.common) {
// Script changed ...
if (checkIsGlobal(obj)) {
if (obj.common.enabled || formerObj.common.enabled) {
adapter.log.info(`Global Script ${id} updated. Restart instance.`);
adapter.restart();
}
} else { // No global script
if (obj.common && obj.common.engine === 'system.adapter.' + adapter.namespace) {
// create states for scripts
createActiveObject(id, obj.common.enabled, () => createProblemObject(id));
}
if ((formerObj.common.enabled && !obj.common.enabled) ||
(formerObj.common.engine === 'system.adapter.' + adapter.namespace && obj.common.engine !== 'system.adapter.' + adapter.namespace)) {
// Script disabled
if (formerObj.common.enabled && formerObj.common.engine === 'system.adapter.' + adapter.namespace) {
// Remove it from executing
stop(id);
}
} else if ((!formerObj.common.enabled && obj.common.enabled) ||
(formerObj.common.engine !== 'system.adapter.' + adapter.namespace && obj.common.engine === 'system.adapter.' + adapter.namespace)) {
// Script enabled
if (obj.common.enabled && obj.common.engine === 'system.adapter.' + adapter.namespace) {
// Start script
load(id);
}
} else { //if (obj.common.source !== formerObj.common.source) {
// Source changed => restart it
stopCounters[id] = stopCounters[id] ? stopCounters[id] + 1 : 1;
stop(id, (res, _id) =>
// only start again after stop when "last" object change to prevent problems on
// multiple changes in fast frequency
!--stopCounters[id] && load(_id));
}
}
}
},
stateChange: (id, state) => {
if (context.interimStateValues[id] !== undefined) {
// any update invalidates the remembered interim value
delete context.interimStateValues[id];
}
if (!id || id.startsWith('messagebox.') || id.startsWith('log.')) {
return;
}
if (id === `${adapter.namespace}.debug.to` && state && !state.ack) {
return !debugMode && debugSendToInspector(state.val);
}
// When still in initializing: already remember current values,
// but data structures are initialized elsewhere
if (!statesInitDone) {
if (state) {
context.states[id] = state;
}
return;
}
const oldState = context.states[id];
if (state) {
if (oldState) {
// enable or disable script
if (!state.ack && id.startsWith(activeStr) && context.objects[id] && context.objects[id].native && context.objects[id].native.script) {
adapter.extendForeignObject(context.objects[id].native.script, { common: { enabled: state.val } });
}
// monitor if adapter is alive and send all subscriptions once more, after adapter goes online
if (/*oldState && */oldState.val === false && state.val && id.endsWith('.alive')) {
if (context.adapterSubs[id]) {
const parts = id.split('.');
const a = parts[2] + '.' + parts[3];
for (let t = 0; t < context.adapterSubs[id].length; t++) {
adapter.log.info(`Detected coming adapter "${a}". Send subscribe: ${context.adapterSubs[id][t]}`);
adapter.sendTo(a, 'subscribe', context.adapterSubs[id][t]);
}
}
}
} else if (/*!oldState && */!context.stateIds.includes(id)) {
context.stateIds.push(id);
context.stateIds.sort();
}
context.states[id] = state;
} else {
if (oldState) delete context.states[id];
state = {};
const pos = context.stateIds.indexOf(id);
if (pos !== -1) {
context.stateIds.splice(pos, 1);
}
}
const _eventObj = eventObj.createEventObject(context, id, context.convertBackStringifiedValues(id, state), context.convertBackStringifiedValues(id, oldState));
// if this state matches any subscriptions
for (let i = 0, l = context.subscriptions.length; i < l; i++) {
const sub = context.subscriptions[i];
if (sub && patternMatching(_eventObj, sub.patternCompareFunctions)) {
try {
sub.callback(_eventObj);
} catch (err) {
adapter.log.error(`Error in callback: ${err}`);
}
}
}
},
fileChange: (id, fileName, size) => {
// if this file matches any subscriptions
for (let i = 0, l = context.subscriptionsFile.length; i < l; i++) {
const sub = context.subscriptionsFile[i];
if (sub && fileMatching(sub, id, fileName)) {
try {
sub.callback(id, fileName, size, sub.withFile);
} catch (err) {
adapter.log.error(`Error in callback: ${err}`);
}
}
}
},
unload: callback => {
debugStop()
.then(() => {
stopTimeSchedules();
setStateCountCheckInterval && clearInterval(setStateCountCheckInterval);
stopAllScripts(callback);
});
},
ready: () => {
adapter.config.maxSetStatePerMinute = parseInt(adapter.config.maxSetStatePerMinute, 10) || 1000;
adapter.config.maxTriggersPerScript = parseInt(adapter.config.maxTriggersPerScript, 10) || 100;
if (adapter.supportsFeature && adapter.supportsFeature('PLUGINS')) {
const sentryInstance = adapter.getPluginInstance('sentry');
if (sentryInstance) {
const Sentry = sentryInstance.getSentryObject();
if (Sentry) {
Sentry.configureScope(scope => {
scope.addEventProcessor((event, _hint) => {
if (event.exception && event.exception.values && event.exception.values[0]) {
const eventData = event.exception.values[0];
if (eventData.stacktrace && eventData.stacktrace.frames && Array.isArray(eventData.stacktrace.frames) && eventData.stacktrace.frames.length) {
// Exclude event if script Marker is included
if (eventData.stacktrace.frames.find(frame => frame.filename && frame.filename.includes(SCRIPT_CODE_MARKER))) {
return null;
}
//Exclude event if own directory is included but not inside own node_modules
const ownNodeModulesDir = nodePath.join(__dirname, 'node_modules');
if (!eventData.stacktrace.frames.find(frame => frame.filename && frame.filename.includes(__dirname) && !frame.filename.includes(ownNodeModulesDir))) {
return null;
}
// We have exception data and do not sorted it out, so report it
return event;
}
}
// No exception in it ... do not report
return null;
});
main();
});
} else {
main();
}
} else {
main();
}
} else {
main();
}
},
message: obj => {
if (obj) {
switch (obj.command) {
// process messageTo commands
case 'toScript':
case 'jsMessageBus':
if (obj.message && (
obj.message.instance === null ||
obj.message.instance === undefined ||
('javascript.' + obj.message.instance === adapter.namespace) ||
(obj.message.instance === adapter.namespace)
)) {
Object.keys(context.messageBusHandlers).forEach(name => {
// script name could be script.js.xxx or only xxx
if ((!obj.message.script || obj.message.script === name) && context.messageBusHandlers[name][obj.message.message]) {
context.messageBusHandlers[name][obj.message.message].forEach(handler => {
try {
if (obj.callback) {
handler.cb.call(handler.sandbox, obj.message.data, result =>
adapter.sendTo(obj.from, obj.command, result, obj.callback));
} else {
handler.cb.call(handler.sandbox, obj.message.data, result => {/* nop */ });
}
} catch (e) {
adapter.setState('scriptProblem.' + name.substring(SCRIPT_CODE_MARKER.length), true, true);
context.logError('Error in callback', e);
}
});
}
});
}
break;
case 'loadTypings': { // Load typings for the editor
const typings = {};
// try to load TypeScript lib files from disk
try {
const typescriptLibs = resolveTypescriptLibs(targetTsLib);
Object.assign(typings, typescriptLibs);
} catch (e) { /* ok, no lib then */
}
// provide the already-loaded ioBroker typings and global script declarations
Object.assign(typings, tsAmbient);
// also provide the known global declarations for each global script
for (const globalScriptPaths of Object.keys(knownGlobalDeclarationsByScript)) {
typings[`${globalScriptPaths}.d.ts`] = knownGlobalDeclarationsByScript[globalScriptPaths];
}
if (obj.callback) {
adapter.sendTo(obj.from, obj.command, {typings}, obj.callback);
}
break;
}
case 'calcAstroAll': {
if (obj.message) {
const sunriseOffset = parseInt(obj.message.sunriseOffset === undefined ? adapter.config.sunriseOffset : obj.message.sunriseOffset, 10) || 0;
const sunsetOffset = parseInt(obj.message.sunsetOffset === undefined ? adapter.config.sunsetOffset : obj.message.sunsetOffset, 10) || 0;
const longitude = parseFloat(obj.message.longitude === undefined ? adapter.config.longitude : obj.message.longitude) || 0;
const latitude = parseFloat(obj.message.latitude === undefined ? adapter.config.latitude : obj.message.latitude) || 0;
const today = getAstroStartOfDay();
let astroEvents = {};
try {
astroEvents = mods.suncalc.getTimes(today, latitude, longitude);
} catch (e) {
adapter.log.error(`Cannot calculate astro data: ${e}`);
}
if (astroEvents) {
try {
astroEvents.nextSunrise = getAstroEvent(
today,
obj.message.sunriseEvent || adapter.config.sunriseEvent,
obj.message.sunriseLimitStart || adapter.config.sunriseLimitStart,
obj.message.sunriseLimitEnd || adapter.config.sunriseLimitEnd,
sunriseOffset,
false,
latitude,
longitude,
true
);
astroEvents.nextSunset = getAstroEvent(
today,
obj.message.sunsetEvent || adapter.config.sunsetEvent,
obj.message.sunsetLimitStart || adapter.config.sunsetLimitStart,
obj.message.sunsetLimitEnd || adapter.config.sunsetLimitEnd,
sunsetOffset,
true,
latitude,
longitude,
true
);
} catch (e) {
adapter.log.error(`Cannot calculate astro data: ${e}`);
}
}
const result = {};
const keys = Object.keys(astroEvents).sort((a, b) => astroEvents[a] - astroEvents[b]);
keys.forEach(key => result[key] = { serverTime: formatHoursMinutesSeconds(astroEvents[key]), date: astroEvents[key].toISOString() });
obj.callback && adapter.sendTo(obj.from, obj.command, result, obj.callback);
}
break;
}
case 'calcAstro': {
if (obj.message) {
const sunriseOffset = parseInt(obj.message.sunriseOffset === undefined ? adapter.config.sunriseOffset : obj.message.sunriseOffset, 10) || 0;
const sunsetOffset = parseInt(obj.message.sunsetOffset === undefined ? adapter.config.sunsetOffset : obj.message.sunsetOffset, 10) || 0;
const longitude = parseFloat(obj.message.longitude === undefined ? adapter.config.longitude : obj.message.longitude) || 0;
const latitude = parseFloat(obj.message.latitude === undefined ? adapter.config.latitude : obj.message.latitude) || 0;
const today = getAstroStartOfDay();
const nextSunrise = getAstroEvent(
today,
obj.message.sunriseEvent || adapter.config.sunriseEvent,
obj.message.sunriseLimitStart || adapter.config.sunriseLimitStart,
obj.message.sunriseLimitEnd || adapter.config.sunriseLimitEnd,
sunriseOffset,
false,
latitude,
longitude,
true
);
const nextSunset = getAstroEvent(
today,
obj.message.sunsetEvent || adapter.config.sunsetEvent,
obj.message.sunsetLimitStart || adapter.config.sunsetLimitStart,
obj.message.sunsetLimitEnd || adapter.config.sunsetLimitEnd,
sunsetOffset,
true,
latitude,
longitude,
true
);
obj.callback && adapter.sendTo(obj.from, obj.command, {
nextSunrise: {
serverTime: formatHoursMinutesSeconds(nextSunrise),
date: nextSunrise
},
nextSunset: {
serverTime: formatHoursMinutesSeconds(nextSunset),
date: nextSunset
}
}, obj.callback);
}
break;
}
case 'debug': {
!debugMode && debugStart(obj.message);
break;
}
case 'debugStop': {
!debugMode && debugStop()
.then(() => console.log('stopped'));
break;
}
case 'rulesOn': {
context.rulesOpened = obj.message;
console.log('Enable messaging for ' + context.rulesOpened);
break;
}
case 'rulesOff': {
// may be if (context.rulesOpened === obj.message)
console.log('Disable messaging for ' + context.rulesOpened);
context.rulesOpened = null;
break;
}
case 'getIoBrokerDataDir': {
obj.callback && adapter.sendTo(obj.from, obj.command, {
dataDir: utils.getAbsoluteDefaultDataDir(),
sep: nodePath.sep
}, obj.callback);
break;
}
}
}
},
/**
* If the JS-Controller catches an unhandled error, this will be called
* so we have a chance to handle it ourself.
* @param {Error} err
*/
error: err => {
// Identify unhandled errors originating from callbacks in scripts
// These are not caught by wrapping the execution code in try-catch
if (err && typeof err.stack === 'string') {
const scriptCodeMarkerIndex = err.stack.indexOf(SCRIPT_CODE_MARKER);
if (scriptCodeMarkerIndex > -1) {
// This is a script error
let scriptName = err.stack.substr(scriptCodeMarkerIndex);
scriptName = scriptName.substr(0, scriptName.indexOf(':'));
context.logError(scriptName, err);
// Leave the script running for now
// signal to the JS-Controller that we handled the error ourselves
return true;
}
// check if a path contains adaptername but not own node_module
// this regex matched "iobroker.javascript/" if NOT followed by "node_modules"
if (!err.stack.match(/iobroker\.javascript[/\\](?!.*node_modules).*/g)) {
// This is an error without any info on origin (mostly async errors like connection errors)
// also consider it as being from a script
adapter.log.error('An error happened which is most likely from one of your scripts, but the originating script could not be detected.');
adapter.log.error('Error: ' + err.message);
adapter.log.error(err.stack);
// signal to the JS-Controller that we handled the error ourselves
return true;
}
}
}
});
adapter = new utils.Adapter(options);
// handler for logs
adapter.on('log', msg =>
Object.keys(context.logSubscriptions)
.forEach(name =>
context.logSubscriptions[name].forEach(handler => {
if (typeof handler.cb === 'function' && (handler.severity === '*' || handler.severity === msg.severity)) {
handler.sandbox.logHandler = handler.severity || '*';
handler.cb.call(handler.sandbox, msg);
handler.sandbox.logHandler = null;
}
})));
context.adapter = adapter;
return adapter;
}
function updateObjectContext(id, obj) {
if (obj) {
// add state to state ID's list
if (obj.type === 'state') {
if (!context.stateIds.includes(id)) {
context.stateIds.push(id);
context.stateIds.sort();
}
if (context.devices && context.channels) {
const parts = id.split('.');
parts.pop();
const chn = parts.join('.');
context.channels[chn] = context.channels[chn] || [];
context.channels[chn].push(id);
parts.pop();
const dev = parts.join('.');
context.devices[dev] = context.devices[dev] || [];
context.devices[dev].push(id);
}
}
} else {
// delete object from state ID's list
const pos = context.stateIds.indexOf(id);
pos !== -1 && context.stateIds.splice(pos, 1);
if (context.devices && context.channels) {
const parts = id.split('.');
parts.pop();
const chn = parts.join('.');
if (context.channels[chn]) {
const posChn = context.channels[chn].indexOf(id);
posChn !== -1 && context.channels[chn].splice(posChn, 1);
}
parts.pop();
const dev = parts.join('.');
if (context.devices[dev]) {
const posDev = context.devices[dev].indexOf(id);
posDev !== -1 && context.devices[dev].splice(posDev, 1);
}
}
delete context.folderCreationVerifiedObjects[id];
}
if (!obj && context.objects[id]) {
// objects was deleted
removeFromNames(id);
delete context.objects[id];
} else if (obj && !context.objects[id]) {
// object was added
context.objects[id] = obj;
addToNames(obj);
} else if (obj && context.objects[id].common) {
// Object just changed
context.objects[id] = obj;
const n = getName(id);
let nn = context.objects[id].common ? context.objects[id].common.name : '';
if (nn && typeof nn === 'object') {
nn = nn[words.getLanguage()] || nn.en;
}
if (n !== nn) {
if (n) {
removeFromNames(id);
}
if (nn) {
addToNames(obj);
}
}
}
}
function main() {
!debugMode && patchFont()
.then(patched => patched && adapter.log.debug('Font patched'));
// todo
context.errorLogFunction = webstormDebug ? console : adapter.log;
activeStr = `${adapter.namespace}.scriptEnabled.`;
mods.fs = new require('./lib/protectFs')(adapter.log, utils.getAbsoluteDefaultDataDir());
mods['fs/promises'] = mods.fs.promises; // to avoid require('fs/promises');
// try to read TS declarations
try {
tsAmbient = {
'javascript.d.ts': nodeFS.readFileSync(mods.path.join(__dirname, 'lib/javascript.d.ts'), 'utf8')
};
tsServer.provideAmbientDeclarations(tsAmbient);
jsDeclarationServer.provideAmbientDeclarations(tsAmbient);
} catch (e) {
adapter.log.warn(`Could not read TypeScript ambient declarations: ${e.message}`);
// This should not happen, so send a error report to Sentry
if (adapter.supportsFeature && adapter.supportsFeature('PLUGINS')) {
const sentryInstance = adapter.getPluginInstance('sentry');
if (sentryInstance) {
const sentryObject = sentryInstance.getSentryObject();
if (sentryObject) sentryObject.captureException(e);
}
}
// Keep the adapter from crashing when the included typings cannot be read
tsAmbient = {};
}
context.logWithLineInfo = function (level, msg) {
if (msg === undefined) {
return context.logWithLineInfo('info', msg);
}
context.errorLogFunction && context.errorLogFunction[level](msg);
const stack = (new Error().stack).split('\n');
for (let i = 3; i < stack.length; i++) {
if (!stack[i]) {
continue;
}
if (stack[i].match(/runInContext|runInNewContext|javascript\.js:/)) {
break;
}
context.errorLogFunction && context.errorLogFunction[level](fixLineNo(stack[i]));
}
};
context.logWithLineInfo.warn = context.logWithLineInfo.bind(1, 'warn');
context.logWithLineInfo.error = context.logWithLineInfo.bind(1, 'error');
context.logWithLineInfo.info = context.logWithLineInfo.bind(1, 'info');
installLibraries(() => {
// Load the TS declarations for Node.js and all 3rd party modules
loadTypeScriptDeclarations();
getData(() => {
context.scheduler = new Scheduler(adapter.log, Date, mods.suncalc, adapter.config.latitude, adapter.config.longitude);
dayTimeSchedules(adapter, context);
sunTimeSchedules(adapter, context);
timeSchedule(adapter, context);
// Warning. It could have a side-effect in compact mode, so all adapters will accept self signed certificates
if (adapter.config.allowSelfSignedCerts) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
adapter.getObjectView('script', 'javascript', {}, async (err, doc) => {
globalScript = '';
globalDeclarations = '';
knownGlobalDeclarationsByScript = {};
if (doc && doc.rows && doc.rows.length) {
// assemble global script
for (let g = 0; g < doc.rows.length; g++) {
const obj = doc.rows[g].value;
if (checkIsGlobal(obj)) {
if (obj && obj.common) {
const engineType = (obj.common.engineType || '').toLowerCase();
if (obj.common.enabled) {
if (engineType.startsWith('typescript')) {
// TypeScript
adapter.log.info(`${obj._id}: compiling TypeScript source...`);
// In order to compile global TypeScript, we need to do some transformations
// 1. For top-level-await, some statements must be wrapped in an immediately-invoked async function
// 2. If any global script uses `import`, the declarations are no longer visible if they are not exported with `declare global`
const transformedSource = transformScriptBeforeCompilation(obj.common.source, true);
// The source code must be transformed in order to support top level await
// Global scripts must not be treated as a module, otherwise their methods
// cannot be found by the normal scripts
// We need to hash both global declarations that are known until now