iobroker.javascript
Version:
Rules Engine for ioBroker
1,040 lines (1,039 loc) • 136 kB
JavaScript
"use strict";
/*
* Javascript adapter
*
* The MIT License (MIT)
*
* Copyright (c) 2014-2024 bluefox <dogafox@gmail.com>,
*
* Copyright (c) 2014 hobbyquaker
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_vm_1 = require("node:vm");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_child_process_1 = require("node:child_process");
const virtual_tsc_1 = require("virtual-tsc");
const node_util_1 = require("node:util");
const prettier_1 = __importDefault(require("prettier"));
const dgram = __importStar(require("node:dgram"));
const crypto = __importStar(require("node:crypto"));
const dns = __importStar(require("node:dns"));
const events = __importStar(require("node:events"));
const http = __importStar(require("node:http"));
const https = __importStar(require("node:https"));
const http2 = __importStar(require("node:http2"));
const net = __importStar(require("node:net"));
const os = __importStar(require("node:os"));
const path = __importStar(require("node:path"));
const util = __importStar(require("node:util"));
const child_process = __importStar(require("node:child_process"));
const stream = __importStar(require("node:stream"));
const zlib = __importStar(require("node:zlib"));
// @ts-expect-error no types available
const suncalc = __importStar(require("suncalc2"));
const axios = __importStar(require("axios"));
// @ts-expect-error no types available
const wake_on_lan = __importStar(require("wake_on_lan"));
const nodeSchedule = __importStar(require("node-schedule"));
const adapter_core_1 = require("@iobroker/adapter-core");
const mirror_1 = require("./lib/mirror");
const protectFs_1 = __importDefault(require("./lib/protectFs"));
const words_1 = require("./lib/words");
const sandbox_1 = require("./lib/sandbox");
const nodeModulesManagement_1 = require("./lib/nodeModulesManagement");
const eventObj_1 = require("./lib/eventObj");
const scheduler_1 = require("./lib/scheduler");
const typescriptSettings_1 = require("./lib/typescriptSettings");
const tools_1 = require("./lib/tools");
const typescriptTools_1 = require("./lib/typescriptTools");
const crypto_1 = require("./lib/crypto");
/**
* 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!!
*/
const forbiddenMirrorLocations = [
'backup-objects',
'files',
'backitup',
'../backups',
'../node_modules',
'../log',
];
const packageJson = JSON.parse((0, node_fs_1.readFileSync)(`${__dirname}/../package.json`).toString());
const SCRIPT_CODE_MARKER = 'script.js.';
let webstormDebug;
const isCI = !!process.env.CI;
// ambient declarations for typescript
let tsAmbient;
// TypeScript's scripts are only recompiled if their source hash changes.
// If an adapter update fixes the compilation bugs, a user won't notice until the changes and re-save 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}`;
// 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 + // the 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());
}
const regExGlobalOld = /_global$/;
const regExGlobalNew = /script\.js\.global\./;
function checkIsGlobal(obj) {
return obj?.common && (regExGlobalOld.test(obj.common.name) || regExGlobalNew.test(obj._id));
}
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;
}
function getNextTimeEvent(time, useNextDay) {
const now = getAstroStartOfDay();
const [timeHours, timeMinutes] = time.split(':');
const nTimeHours = parseInt(timeHours, 10);
const nTimeMinutes = parseInt(timeMinutes, 10);
if (useNextDay &&
(now.getHours() > nTimeHours || (now.getHours() === nTimeHours && now.getMinutes() > nTimeMinutes))) {
now.setDate(now.getDate() + 1);
}
now.setHours(nTimeHours);
now.setMinutes(nTimeMinutes);
return now;
}
function getAstroStartOfDay() {
const d = new Date();
d.setMinutes(0);
d.setSeconds(0);
d.setMilliseconds(0);
d.setTime(d.getTime() - d.getTimezoneOffset() * 60 * 1000);
d.setUTCHours(0);
return d;
}
function formatHoursMinutesSeconds(date) {
const h = String(date.getHours());
const m = String(date.getMinutes());
const s = String(date.getSeconds());
return `${h.padStart(2, '0')}:${m.padStart(2, '0')}:${s.padStart(2, '0')}`;
}
// Due to a npm bug, virtual-tsc may be hoisted to the top level node_modules, but
// TypeScript may still be in the adapter level (https://npm.community/t/packages-with-peerdependencies-are-incorrectly-hoisted/4794),
// so we need to tell virtual-tsc where TypeScript is
(0, virtual_tsc_1.setTypeScriptResolveOptions)({
paths: [require.resolve('typescript')],
});
// compiler instance for global JS declarations
const jsDeclarationServer = new virtual_tsc_1.Server(typescriptSettings_1.jsDeclarationCompilerOptions, isCI ? false : undefined);
/**
* Stores the IDs of script objects whose change should be ignored because
* the compiled source was just updated
*/
class JavaScript extends adapter_core_1.Adapter {
context;
errorLogFunction = {
error: (msg) => console.error(msg),
warn: (msg) => console.warn(msg),
info: (msg) => console.log(msg),
debug: (msg) => console.debug(msg),
silly: (msg) => console.debug(msg),
};
mods;
objectsInitDone = false;
statesInitDone = false;
objects = {};
states = {};
interimStateValues = {};
stateIds = [];
subscriptions = [];
subscriptionsFile = [];
subscriptionsObject = [];
subscribedPatterns = {};
subscribedPatternsFile = {};
adapterSubs = {};
timers = {};
_enums = [];
names = {}; // name: id
scripts = {};
password = '';
messageBusHandlers = {};
logSubscriptions = {};
tempDirectories = {}; // name: path
folderCreationVerifiedObjects = {};
/** if logs are subscribed or not */
logSubscribed = false;
timeSettings = { format12: false, leadingZeros: true };
dayScheduleTimer = null; // schedule for astrological day
sunScheduleTimer = null; // schedule for sun moment times
timeScheduleTimer = null; // schedule for astrological day
activeStr = ''; // enabled state prefix
mirror;
stopCounters = {};
setStateCountCheckInterval = null;
globalScript = '';
/** Generated declarations for global TypeScripts */
globalDeclarations = '';
// Remember which definitions the global scripts
// have access to, because it depends on the compilation order
knownGlobalDeclarationsByScript = {};
globalScriptLines = 0;
// compiler instance for typescript
tsServer;
ignoreObjectChange = new Set();
debugState = {
scriptName: '',
child: null,
promiseOnEnd: null,
paused: false,
started: 0,
running: false,
};
constructor(options = {}) {
options = {
...options,
name: 'javascript', // adapter name
useFormatDate: true,
/**
* If the JS-Controller catches an unhandled error, this will be called,
* so we have a chance to handle it ourselves.
*/
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.substring(scriptCodeMarkerIndex);
scriptName = scriptName.substring(0, scriptName.indexOf(':'));
this.logError(scriptName, 'Error:', 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
this.log.error('An error happened which is most likely from one of your scripts, but the originating script could not be detected.');
this.log.error(`Error: ${err.message}`);
this.log.error(err.stack);
// signal to the JS-Controller that we handled the error ourselves
return true;
}
}
return false;
},
};
super(options);
this.on('objectChange', this.onObjectChange.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
this.on('ready', this.onReady.bind(this));
this.on('message', this.onMessage.bind(this));
this.on('unload', this.onUnload.bind(this));
this.on('fileChange', this.onFileChange.bind(this));
this.on('log', this.onLog.bind(this));
this.mods = {
fs: {},
'fs/promises': {},
dgram,
crypto,
dns,
events,
http,
https,
http2,
net,
os,
path,
util,
child_process,
stream,
zlib,
suncalc,
axios,
wake_on_lan,
nodeSchedule,
};
// check the webstorm debug and just debug modes
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];
}
}
}
}
this.context = {
mods: this.mods,
objects: this.objects,
states: this.states,
interimStateValues: this.interimStateValues,
stateIds: this.stateIds,
errorLogFunction: this.errorLogFunction,
subscriptions: this.subscriptions,
subscriptionsFile: this.subscriptionsFile,
subscriptionsObject: this.subscriptionsObject,
subscribedPatterns: this.subscribedPatterns,
subscribedPatternsFile: this.subscribedPatternsFile,
adapterSubs: this.adapterSubs,
cacheObjectEnums: {},
timers: this.timers,
enums: this._enums,
names: this.names,
scripts: this.scripts,
messageBusHandlers: this.messageBusHandlers,
logSubscriptions: this.logSubscriptions,
tempDirectories: this.tempDirectories,
folderCreationVerifiedObjects: this.folderCreationVerifiedObjects,
isEnums: false, // If some subscription wants enum
channels: null,
devices: null,
logWithLineInfo: this.logWithLineInfo.bind(this),
scheduler: null,
timerId: 0,
rulesOpened: null, // opened rules
language: this.language || 'en',
updateLogSubscriptions: this.updateLogSubscriptions.bind(this),
convertBackStringifiedValues: this.convertBackStringifiedValues.bind(this),
updateObjectContext: this.updateObjectContext.bind(this),
prepareStateObject: this.prepareStateObject.bind(this),
debugMode,
getAbsoluteDefaultDataDir: adapter_core_1.getAbsoluteDefaultDataDir,
adapter: this,
logError: this.logError.bind(this),
allowSelfSignedCerts: false,
};
this.tsServer = new virtual_tsc_1.Server(typescriptSettings_1.tsCompilerOptions, this.tsLog);
}
async onObjectChange(id, obj) {
// Check if we should ignore this change (once!) because we just updated the compiled sources
if (this.ignoreObjectChange.has(id)) {
// Update the cached script object and do nothing more
this.objects[id] = obj;
this.ignoreObjectChange.delete(id);
return;
}
// When still in initializing: already remember current values,
// but data structures are initialized elsewhere
if (!this.objectsInitDone) {
if (obj) {
this.objects[id] = obj;
}
return;
}
if (id.startsWith('enum.')) {
// clear cache
this.context.cacheObjectEnums = {};
// update this._enums array
if (obj) {
// If new
if (!this._enums.includes(id)) {
this._enums.push(id);
this._enums.sort();
}
}
else {
const pos = this._enums.indexOf(id);
// if deleted
if (pos !== -1) {
this._enums.splice(pos, 1);
}
}
}
if (id === 'system.config' && obj?.common?.language) {
// set language for debug messages
(0, words_1.setLanguage)(obj.common.language);
this.language = obj.common.language;
this.context.language = this.language;
}
// update stored time format for variables.dayTime
if (id === `${this.namespace}.variables.dayTime` && obj?.native) {
this.timeSettings.format12 = obj.native.format12 || false;
this.timeSettings.leadingZeros = obj.native.leadingZeros === undefined ? true : obj.native.leadingZeros;
}
// send changes to the disk mirror
this.mirror?.onObjectChange(id, obj);
const formerObj = this.objects[id];
this.updateObjectContext(id, obj); // Update all Meta object data
// for the alias object changes on the state objects, we need to manually update the
// state cache value, because the new value is only published on the next change
if (obj?.type === 'state' && id.startsWith('alias.0.')) {
// execute async for speed
this.getForeignStateAsync(id)
.then(state => {
if (state) {
this.states[id] = state;
}
else if (this.states[id] !== undefined) {
delete this.states[id];
}
})
.catch(() => {
/* ignore */
});
}
this.subscriptionsObject.forEach(sub => {
// ToDo: implement comparing with id.0.* too
if (sub.pattern === id) {
try {
sub.callback(id, obj);
}
catch (err) {
this.log.error(`Error in callback: ${err.toString()}`);
}
}
});
// handle Script object updates
if (!obj && 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.common.enabled) {
this.log.info(`Active global Script ${id} deleted. Restart instance.`);
this.restart();
}
}
else if (formerObj.common?.engine === `system.adapter.${this.namespace}`) {
// It was a non-global Script and deleted => stop and remove it
await this.stopScript(id);
// delete scriptEnabled.blabla variable
const idActive = `scriptEnabled.${id.substring(SCRIPT_CODE_MARKER.length)}`;
await this.delStateAsync(idActive);
await this.delObjectAsync(idActive);
// delete scriptProblem.blabla variable
const idProblem = `scriptProblem.${id.substring(SCRIPT_CODE_MARKER.length)}`;
await this.delStateAsync(idProblem);
await this.delObjectAsync(idProblem);
}
}
else if (!formerObj && obj?.type === 'script') {
// New script that does not exist before
if (checkIsGlobal(obj)) {
// new global script added => restart adapter
if (obj.common.enabled) {
this.log.info(`Active global Script ${id} created. Restart instance.`);
this.restart();
}
}
else if (obj.common?.engine === `system.adapter.${this.namespace}`) {
// new non-global script - create states for scripts
await this.createActiveObject(id, !!obj.common.enabled);
await this.createProblemObject(id);
if (obj.common.enabled) {
// if enabled => Start a script
await this.loadScriptById(id);
}
}
}
else if (obj?.type === 'script' && formerObj?.common) {
// Script changed ...
if (checkIsGlobal(obj)) {
if (obj.common.enabled || formerObj.common.enabled) {
this.log.info(`Global Script ${id} updated. Restart instance.`);
this.restart();
}
}
else {
// No global script
if (obj.common?.engine === `system.adapter.${this.namespace}`) {
// create states for scripts
await this.createActiveObject(id, !!obj.common.enabled);
await this.createProblemObject(id);
}
if ((formerObj.common.enabled && !obj.common.enabled) ||
(formerObj.common.engine === `system.adapter.${this.namespace}` &&
obj.common.engine !== `system.adapter.${this.namespace}`)) {
// Script disabled
if (formerObj.common.enabled && formerObj.common.engine === `system.adapter.${this.namespace}`) {
// Remove it from executing
await this.stopScript(id);
}
}
else if ((!formerObj.common.enabled && obj.common.enabled) ||
(formerObj.common.engine !== `system.adapter.${this.namespace}` &&
obj.common.engine === `system.adapter.${this.namespace}`)) {
// Script enabled
if (obj.common.enabled && obj.common.engine === `system.adapter.${this.namespace}`) {
// Start script
await this.loadScriptById(id);
}
}
else {
// if (obj.common.source !== formerObj.common.source) {
// Source changed => restart the script
this.stopCounters[id] = this.stopCounters[id] ? this.stopCounters[id] + 1 : 1;
void this.stopScript(id).then(() => {
// only start again after stop when "last" object change to prevent problems on
// multiple changes in fast frequency
if (!--this.stopCounters[id]) {
void this.loadScriptById(id);
}
});
}
}
}
}
onStateChange(id, state) {
if (this.interimStateValues[id] !== undefined) {
// any update invalidates the remembered interim value
delete this.interimStateValues[id];
}
if (!id || id.startsWith('messagebox.') || id.startsWith('log.')) {
return;
}
if (id === `${this.namespace}.debug.to` && state && !state.ack) {
if (!this.context.debugMode) {
this.debugSendToInspector(state.val);
}
return;
}
// When still in initializing: already remember current values,
// but data structures are initialized elsewhere
if (!this.statesInitDone) {
if (state) {
this.states[id] = state;
}
return;
}
const oldState = this.states[id];
if (state) {
if (oldState) {
// enable or disable script
if (!state.ack && id.startsWith(this.activeStr) && this.objects[id]?.native?.script) {
void this.extendForeignObject(this.objects[id].native.script, {
common: { enabled: state.val },
});
}
// monitor if the adapter is alive and send all subscriptions once more, after the adapter goes online
if ( /*oldState && */oldState.val === false && state.val && id.endsWith('.alive')) {
if (this.adapterSubs[id]) {
const parts = id.split('.');
const a = `${parts[2]}.${parts[3]}`;
for (let t = 0; t < this.adapterSubs[id].length; t++) {
this.log.info(`Detected coming adapter "${a}". Send subscribe: ${this.adapterSubs[id][t]}`);
this.sendTo(a, 'subscribe', this.adapterSubs[id][t]);
}
}
}
}
else if ( /*!oldState && */!this.stateIds.includes(id)) {
this.stateIds.push(id);
this.stateIds.sort();
}
this.states[id] = state;
}
else {
if (oldState) {
delete this.states[id];
}
state = {};
const pos = this.stateIds.indexOf(id);
if (pos !== -1) {
this.stateIds.splice(pos, 1);
}
}
const _eventObj = (0, eventObj_1.createEventObject)(this.context, id, this.convertBackStringifiedValues(id, state), this.convertBackStringifiedValues(id, oldState));
// if this state matches any subscriptions
for (let i = 0, l = this.subscriptions.length; i < l; i++) {
const sub = this.subscriptions[i];
if (sub?.patternCompareFunctions && patternMatching(_eventObj, sub.patternCompareFunctions)) {
try {
sub.callback(_eventObj);
}
catch (err) {
this.log.error(`Error in callback: ${err.toString()}`);
}
}
}
}
onFileChange(id, fileName, size) {
// if this file matches any subscriptions
for (let i = 0, l = this.subscriptionsFile.length; i < l; i++) {
const sub = this.subscriptionsFile[i];
if (sub && fileMatching(sub, id, fileName)) {
try {
sub.callback(id, fileName, size, sub.withFile);
}
catch (err) {
this.log.error(`Error in callback: ${err.toString()}`);
}
}
}
}
async onUnload(callback) {
await this.debugStop();
this.stopTimeSchedules();
if (this.setStateCountCheckInterval) {
clearInterval(this.setStateCountCheckInterval);
this.setStateCountCheckInterval = null;
}
await this.stopAllScripts();
if (typeof callback === 'function') {
callback();
}
}
async onReady() {
this.errorLogFunction = this.log;
this.context.errorLogFunction = this.log;
this.config.maxSetStatePerMinute = parseInt(this.config.maxSetStatePerMinute, 10) || 1000;
this.config.maxTriggersPerScript = parseInt(this.config.maxTriggersPerScript, 10) || 100;
if (this.supportsFeature?.('PLUGINS')) {
const sentryInstance = this.getPluginInstance('sentry');
if (sentryInstance) {
const Sentry = sentryInstance.getSentryObject();
Sentry?.withScope(scope => {
scope.addEventProcessor((event, _hint) => {
if (event.exception?.values?.[0]) {
const eventData = event.exception.values[0];
if (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?.includes(SCRIPT_CODE_MARKER))) {
return null;
}
// Exclude event if own directory is included but not inside own node_modules
const ownNodeModulesDir = (0, node_path_1.join)(__dirname, 'node_modules');
if (!eventData.stacktrace.frames.find(frame => frame.filename?.includes(__dirname) &&
!frame.filename.includes(ownNodeModulesDir))) {
return null;
}
// We have exception data and did not sort it out, so report it
return event;
}
}
// No exception in it ... do not report
return null;
});
});
}
}
await this.main();
}
onMessage(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}` === this.namespace ||
obj.message.instance === this.namespace)) {
Object.keys(this.messageBusHandlers).forEach(name => {
// the script name could be script.js.xxx or only xxx
if ((!obj.message.script || obj.message.script === name) &&
this.messageBusHandlers[name][obj.message.message]) {
this.messageBusHandlers[name][obj.message.message].forEach(handler => {
const sandbox = handler.sandbox;
sandbox.verbose && sandbox.log(`onMessage: ${JSON.stringify(obj.message)}`, 'info');
try {
if (obj.callback) {
handler.cb.call(sandbox, obj.message.data, (result) => {
if (sandbox.verbose) {
sandbox.log(`onMessage result: ${JSON.stringify(result)}`, 'info');
}
this.sendTo(obj.from, obj.command, result, obj.callback);
});
}
else {
handler.cb.call(sandbox, obj.message.data, (result) => {
sandbox.verbose &&
sandbox.log(`onMessage result: ${JSON.stringify(result)}`, 'info');
});
}
}
catch (err) {
void this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, true, true);
this.logError(name, 'Error in callback:', err);
}
});
}
});
}
break;
case 'loadTypings': {
// Load typings for the editor
const typings = {};
// try to load TypeScript lib files from disk
try {
const typescriptLibs = (0, typescriptTools_1.resolveTypescriptLibs)(typescriptSettings_1.targetTsLib);
Object.assign(typings, typescriptLibs);
}
catch {
/* 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(this.knownGlobalDeclarationsByScript)) {
typings[`${globalScriptPaths}.d.ts`] = this.knownGlobalDeclarationsByScript[globalScriptPaths];
}
if (obj.callback) {
this.sendTo(obj.from, obj.command, { typings }, obj.callback);
}
break;
}
case 'calcAstroAll': {
if (obj.message) {
const sunriseOffset = parseInt(obj.message.sunriseOffset === undefined
? this.config.sunriseOffset
: obj.message.sunriseOffset, 10) || 0;
const sunsetOffset = parseInt(obj.message.sunsetOffset === undefined
? this.config.sunsetOffset
: obj.message.sunsetOffset, 10) || 0;
const longitude = parseFloat(obj.message.longitude === undefined ? this.config.longitude : obj.message.longitude) || 0;
const latitude = parseFloat(obj.message.latitude === undefined ? this.config.latitude : obj.message.latitude) ||
0;
const today = getAstroStartOfDay();
let astroEvents = {};
try {
astroEvents = this.mods.suncalc.getTimes(today, latitude, longitude);
}
catch (err) {
this.log.error(`Cannot calculate astro data: ${err}`);
}
if (astroEvents) {
try {
astroEvents.nextSunrise = this.getAstroEvent(today, obj.message.sunriseEvent || this.config.sunriseEvent, obj.message.sunriseLimitStart || this.config.sunriseLimitStart, obj.message.sunriseLimitEnd || this.config.sunriseLimitEnd, sunriseOffset, false, latitude, longitude, true);
astroEvents.nextSunset = this.getAstroEvent(today, obj.message.sunsetEvent || this.config.sunsetEvent, obj.message.sunsetLimitStart || this.config.sunsetLimitStart, obj.message.sunsetLimitEnd || this.config.sunsetLimitEnd, sunsetOffset, true, latitude, longitude, true);
}
catch (err) {
this.log.error(`Cannot calculate astro data: ${err}`);
}
}
const result = {};
const keys = Object.keys(astroEvents).sort((a, b) => astroEvents[a] -
astroEvents[b]);
keys.forEach(key => {
const validDate = astroEvents[key] !== null &&
!isNaN(astroEvents[key].getTime());
result[key] = {
isValidDate: validDate,
serverTime: validDate
? formatHoursMinutesSeconds(astroEvents[key])
: 'n/a',
date: validDate
? astroEvents[key].toISOString()
: 'n/a',
};
});
if (obj.callback) {
this.sendTo(obj.from, obj.command, result, obj.callback);
}
}
break;
}
case 'calcAstro': {
if (obj.message) {
const longitude = parseFloat(obj.message.longitude === undefined ? this.config.longitude : obj.message.longitude) || 0;
const latitude = parseFloat(obj.message.latitude === undefined ? this.config.latitude : obj.message.latitude) ||
0;
const today = getAstroStartOfDay();
const sunriseEvent = obj.message?.sunriseEvent || this.config.sunriseEvent;
const sunriseLimitStart = obj.message?.sunriseLimitStart || this.config.sunriseLimitStart;
const sunriseLimitEnd = obj.message?.sunriseLimitEnd || this.config.sunriseLimitEnd;
const sunriseOffset = parseInt(obj.message.sunriseOffset === undefined
? this.config.sunriseOffset
: obj.message.sunriseOffset, 10) || 0;
const nextSunrise = this.getAstroEvent(today, sunriseEvent, sunriseLimitStart, sunriseLimitEnd, sunriseOffset, false, latitude, longitude, true);
const sunsetEvent = obj.message?.sunsetEvent || this.config.sunsetEvent;
const sunsetLimitStart = obj.message?.sunsetLimitStart || this.config.sunsetLimitStart;
const sunsetLimitEnd = obj.message?.sunsetLimitEnd || this.config.sunsetLimitEnd;
const sunsetOffset = parseInt(obj.message.sunsetOffset === undefined
? this.config.sunsetOffset
: obj.message.sunsetOffset, 10) || 0;
const nextSunset = this.getAstroEvent(today, sunsetEvent, sunsetLimitStart, sunsetLimitEnd, sunsetOffset, true, latitude, longitude, true);
const validDateSunrise = nextSunrise !== null && !isNaN(nextSunrise.getTime());
const validDateSunset = nextSunset !== null && !isNaN(nextSunset.getTime());
this.log.debug(`calcAstro sunrise: ${sunriseEvent} -> start ${sunriseLimitStart}, end: ${sunriseLimitEnd}, offset: ${sunriseOffset} - ${validDateSunrise ? nextSunrise.toISOString() : 'n/a'}`);
this.log.debug(`calcAstro sunset: ${sunsetEvent} -> start ${sunsetLimitStart}, end: ${sunsetLimitEnd}, offset: ${sunsetOffset} - ${validDateSunset ? nextSunset.toISOString() : 'n/a'}`);
if (obj.callback) {
this.sendTo(obj.from, obj.command, {
nextSunrise: {
isValidDate: validDateSunrise,
serverTime: validDateSunrise ? formatHoursMinutesSeconds(nextSunrise) : 'n/a',
date: nextSunrise.toISOString(),
},
nextSunset: {
isValidDate: validDateSunset,
serverTime: validDateSunset ? formatHoursMinutesSeconds(nextSunset) : 'n/a',
date: nextSunset.toISOString(),
},
}, obj.callback);
}
}
break;
}
case 'debug': {
if (!this.context.debugMode) {
this.debugStart(obj.message);
}
break;
}
case 'debugStop': {
if (!this.context.debugMode) {
void this.debugStop().then(() => console.log('stopped'));
}
break;
}
case 'rulesOn': {
this.context.rulesOpened = obj.message;
console.log(`Enable messaging for ${this.context.rulesOpened}`);
break;
}
case 'rulesOff': {
// maybe if (context.rulesOpened === obj.message)
console.log(`Disable messaging for ${this.context.rulesOpened}`);
this.context.rulesOpened = null;
break;
}
case 'getIoBrokerDataDir': {
if (obj.callback) {
this.sendTo(obj.from, obj.command, {
dataDir: (0, adapter_core_1.getAbsoluteDefaultDataDir)(),
sep: node_path_1.sep,
}, obj.callback);
}
break;
}
case 'chatCompletion': {
// Proxy chat completion requests to an OpenAI-compatible API endpoint
if (obj.callback) {
const baseUrl = (obj.message?.baseUrl || '').trim();
const apiKey = (obj.message?.apiKey || '').trim();
const chatModel = (obj.message?.model || '').trim();
const messages = obj.message?.messages;
const provider = (obj.message?.provider || 'openai').trim();
// Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
if (!apiKey &&
(provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
break;
}
if (!chatModel || !messages) {
this.sendTo(obj.from, obj.command, { error: 'Model and messages are required' }, obj.callback);
break;
}
let url;
const chatHeaders = {
'Content-Type': 'application/json',
};
let bodyObj;
if (provider === 'anthropic') {
url = 'https://api.anthropic.com/v1/messages';
chatHeaders['x-api-key'] = apiKey;
chatHeaders['anthropic-version'] = '2023-06-01';
const systemMessages = messages.filter((m) => m.role === 'system');
const nonSystemMessages = messages.filter((m) => m.role !== 'system');
const systemText = systemMessages.map((m) => m.content).join('\n\n');
bodyObj = {
model: chatModel,
max_tokens: 8192,
stream: false,
...(systemText ? { system: systemText } : {}),
messages: nonSystemMessages,
};
}
else if (provider === 'gemini') {
url = 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions';
if (apiKey) {
chatHeaders.Authorization = `Bearer ${apiKey}`;
}
bodyObj = { model: chatModel, messages, stream: false };
}
else if (provider === 'deepseek') {
url = 'https://api.deepseek.com/chat/completions';
chatHeaders.Authorization = `Bearer ${apiKey}`;
bodyObj = { model: chatModel, messages, stream: false };
}
else {
url = `${baseUrl || 'https://api.openai.com/v1'}/chat/completions`;
if (apiKey) {
chatHeaders.Authorization = `Bearer ${apiKey}`;
}
bodyObj = { model: chatModel, messages, stream: false };
}
const body = JSON.stringify(bodyObj);
const bodyBuffer = Buffer.from(body, 'utf8');
chatHeaders['Content-Length'] = bodyBuffer.length;
let urlObj;
try {
urlObj = new URL(url);
}
catch {
this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
break;
}
const isHttps = urlObj.protocol === 'https:';
const requestModule = isHttps ? https : http;
const req = requestModule.request(url, {
method: 'POST',
headers: chatHeaders,
timeout: 600000,
...(isHttps && this.config.allowSelfSignedCerts ? { rejectUnauthorized: false } : {}),
}, res => {
let data = '';
res.on('data', (chunk) => {
data += chunk.toString();
});
res.on('end', () => {
if (res.statusCode === 200) {
try {
const parsed = JSON.parse(data);
const content = provider === 'anthropic'
? parsed.content?.[0]?.text || ''
: parsed.choices?.[0]?.message?.content || '';
if (!content) {
this.sendTo(obj.from, obj.command, { error: 'Empty response from API' }, obj.callback);
}
else {
this.sendTo(obj.from, obj.command, { success: true, content }, obj.callback);
}
}
catch {
this.sendTo(obj.from, obj.command, { error: 'Invalid JSON response from API' }, obj.callback);
}
}
else {
let errorMsg = `API returned status ${res.statusCode}`;
try {
const parsed = JSON.parse(data);
if (parsed.error?.message) {
errorMsg = parsed.error.message;
}
}
catch {
// ignore parse error
}
this.sendTo(obj.from, obj.command, { error: errorMsg }, obj.callback);
}
});
});
req.on('error', (err) => {
this.sendTo(obj.from, obj.command, { error: `Connection failed: ${err.message}` }, obj.callback);
});
req.on('timeout', () => {
req.destroy();
this.sendTo(obj.from, obj.command, { error: 'Connection timeout (600s)' }, obj.callback);
});
req.write(bodyBuffer);
req.end();
}
break;
}
case 'testApiConnection': {
// Test connection to an OpenAI-compatible API endpoint
if (obj.callback) {
const baseUrl = (obj.message?.baseUrl || '').trim();
const apiKey = (obj.message?.apiKey || '').trim();
const provider = (obj.message?.provider || 'openai').trim();
// Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
if (!apiKey &&