iobroker.javascript
Version:
Rules Engine for ioBroker
1,070 lines • 162 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 aiProviderResolver_1 = require("./lib/aiProviderResolver");
const anthropicAdapter_1 = require("./lib/anthropicAdapter");
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
*/
const HTTP_STATUS_TEXTS = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
429: 'Too Many Requests / Rate Limit',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
};
function httpStatusText(code) {
return HTTP_STATUS_TEXTS[code] ?? `Error ${code}`;
}
/**
* Resolves the correct http/https module based on the URL string.
* Returns null if the URL is invalid.
*/
function resolveRequestModule(url) {
try {
const { protocol } = new URL(url);
const isHttps = protocol === 'https:';
return { module: isHttps ? https : http, isHttps };
}
catch {
return null;
}
}
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 = [];
/** Fast O(1) lookup set – always kept in sync with stateIds */
stateIdSet = new Set();
subscriptions = [];
/**
* O(1) dispatch map for subscriptions with exact (non-wildcard) string IDs.
* Always kept in sync with `subscriptions`.
*/
subscriptionsMap = new Map();
/**
* Subscriptions whose pattern.id is a RegExp, contains wildcards (*,?), or is undefined.
* These must still be checked linearly on every state change.
*/
subscriptionsWildcard = [];
subscriptionsFile = [];
subscriptionsObject = [];
/** O(1) dispatch map for subscriptionsObject – pattern → subscribers */
subscriptionsObjectMap = new Map();
/** IO-9: Cache for sendTo broadcast – adapterName → instance list, invalidated on object change */
sendToInstanceCache = new Map();
subscribedPatterns = {};
subscribedPatternsFile = {};
adapterSubs = {};
timers = {};
/** Reverse-index: scriptName → Set of stateIds that have timers for this script – O(1) cleanup */
timersByScript = new Map();
/** O(1) Set for enum-id lookups – replaces sorted string[] array */
_enums = new Set();
names = {}; // name: id
/** Reverse map: id → name for O(1) getName() lookups */
nameById = new Map();
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;
/**
* Decrypted AI API keys cached from the central credential store (manager mode),
* keyed by credential ID (e.g. `system.credentials.anthropic`). Kept fresh by the
* subscriptions set up in `subscribeAiCredentials`.
*/
aiCredentialCache = new Map();
/** Unsubscribe callbacks for the AI credential subscriptions (manager mode). */
aiCredentialUnsubscribers = [];
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;
/** Running counter to build unique names for ad-hoc scripts started via the "execute" message */
executeCounter = 0;
// compiler instance for typescript
tsServer;
logCollectors = [];
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,
subscriptionsMap: this.subscriptionsMap,
subscriptionsWildcard: this.subscriptionsWildcard,
subscriptionsFile: this.subscriptionsFile,
subscriptionsObject: this.subscriptionsObject,
subscriptionsObjectMap: this.subscriptionsObjectMap,
sendToInstanceCache: this.sendToInstanceCache,
subscribedPatterns: this.subscribedPatterns,
subscribedPatternsFile: this.subscribedPatternsFile,
adapterSubs: this.adapterSubs,
cacheObjectEnums: {},
timers: this.timers,
timersByScript: this.timersByScript,
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 Set
if (obj) {
this._enums.add(id);
}
else {
this._enums.delete(id);
}
}
// IO-9: Invalidate sendTo instance-cache when adapter instances change
if (id.startsWith('system.adapter.')) {
const parts = id.split('.');
if (parts.length >= 3) {
const adapterName = parts[2]; // e.g. "zigbee" from "system.adapter.zigbee.0"
this.sendToInstanceCache.delete(adapterName);
}
}
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 */
});
}
// O(1) dispatch via pattern map instead of O(n) forEach
const objSubs = this.subscriptionsObjectMap.get(id);
if (objSubs) {
for (const sub of objSubs) {
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.engine === `system.adapter.${this.namespace}` ||
formerObj.common.engine === `system.adapter.${this.namespace}`) {
// Source changed => restart the script (only on the relevant instance)
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 (const sub of this.adapterSubs[id]) {
this.log.info(`Detected coming adapter "${a}". Send subscribe: ${sub}`);
this.sendTo(a, 'subscribe', sub);
}
}
}
}
else if ( /*!oldState && */!this.stateIdSet.has(id)) {
this.sortedInsert(id);
this.stateIdSet.add(id);
}
this.states[id] = state;
}
else {
if (oldState) {
delete this.states[id];
}
state = {};
const pos = this.binaryIndexOf(this.stateIds, id);
if (pos !== -1) {
this.stateIds.splice(pos, 1);
this.stateIdSet.delete(id);
}
}
// Collect matching subscriptions:
// 1. O(1) exact-id map lookup – only buckets for this specific state id
// 2. Linear scan over wildcard/regex subscriptions (unavoidable)
// EventObj is created lazily – only when at least one subscription must be dispatched.
const exactSubs = this.subscriptionsMap.get(id);
const wildcardSubs = this.subscriptionsWildcard;
const hasWork = (exactSubs && exactSubs.length > 0) || wildcardSubs.length > 0;
if (!hasWork) {
return;
}
let _eventObj = null;
const getEvent = () => {
if (!_eventObj) {
_eventObj = (0, eventObj_1.createEventObject)(this.context, id, this.convertBackStringifiedValues(id, state), this.convertBackStringifiedValues(id, oldState));
}
return _eventObj;
};
if (exactSubs) {
for (let i = 0, l = exactSubs.length; i < l; i++) {
const sub = exactSubs[i];
if (sub?.patternCompareFunctions && patternMatching(getEvent(), sub.patternCompareFunctions)) {
try {
sub.callback(getEvent());
}
catch (err) {
this.log.error(`Error in callback: ${err.toString()}`);
}
}
}
}
// if this state matches any subscriptions
for (let i = 0, l = wildcardSubs.length; i < l; i++) {
const sub = wildcardSubs[i];
if (sub?.patternCompareFunctions && patternMatching(getEvent(), sub.patternCompareFunctions)) {
try {
sub.callback(getEvent());
}
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) {
try {
await this.debugStop();
this.stopTimeSchedules();
if (this.setStateCountCheckInterval) {
clearInterval(this.setStateCountCheckInterval);
this.setStateCountCheckInterval = null;
}
await this.unsubscribeAiCredentials();
await this.stopAllScripts();
}
catch (err) {
this.log.error(`Error during unload: ${err.message}`);
}
finally {
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();
}
/** Read and decrypt a single AI credential's key from the central store; returns '' (and logs) on error. */
async readAiCredentialKey(id) {
if (!adapter_core_1.Credentials?.getCredentials) {
this.log.warn(`Cannot read AI credential "${id}": Credentials API is only with 7.2 js-controller available`);
return '';
}
try {
const cred = await adapter_core_1.Credentials.getCredentials(this, id);
return (cred?.values?.key || '').trim();
}
catch (e) {
this.log.warn(`Cannot read AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
return '';
}
}
/**
* Resolve the API key (and base URL) for an AI provider.
*
* In `manual` mode the key comes from the encryptedNative adapter config.
* In `manager` mode the config only stores the ID of a credential in the central
* ioBroker credential store (`system.credentials.*`); the actual key is taken from the
* `aiCredentialCache` (kept fresh by `subscribeAiCredentials`) or, for credentials we are
* not subscribed to (e.g. a not-yet-saved selection in the settings dialog), read directly.
*
* The settings-dialog Test button may pass form values that are not saved yet
* (`messageApiKey` / `messageCredentialId` / `credentialType`); those win over the stored config.
*/
async resolveAiCredentials(provider, opts = {}) {
const mode = opts.credentialType || this.config.credentialType || 'manual';
if (mode === 'manager') {
// The base URL is not a secret and is resolved the same way in both modes.
const { baseUrl } = (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
const id = (opts.messageCredentialId || (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider)).trim();
if (!id) {
return { apiKey: '', baseUrl };
}
// Prefer the cached value kept fresh by the credential subscription.
const cached = this.aiCredentialCache.get(id);
const apiKey = cached !== undefined ? cached : await this.readAiCredentialKey(id);
return { apiKey, baseUrl };
}
// Manual mode. The Test button sends the current form key (maybe empty) — let it win.
if (opts.messageApiKey !== undefined) {
return (0, aiProviderResolver_1.resolveTestCredentials)(this.config, provider, opts.messageApiKey, opts.messageBaseUrl);
}
return (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
}
/**
* In `manager` mode, subscribe to all configured AI credentials so that edits made in the
* admin credential manager (Settings → Credentials) are picked up live, without restarting
* the adapter (the `system.credentials.*` objects are global, not part of the instance config).
* The decrypted keys are cached and kept fresh by the subscription handlers.
*/
async subscribeAiCredentials() {
// Always start from a clean state (idempotent — also used to re-subscribe).
await this.unsubscribeAiCredentials();
if (this.config.credentialType !== 'manager') {
return;
}
if (!adapter_core_1.Credentials?.subscribeCredentials) {
this.log.warn(`Cannot subscribe AI credential: Credentials API is only with 7.2 js-controller available`);
return;
}
// Collect the distinct credential IDs configured across all AI providers.
const ids = new Set();
for (const provider of ['openai', 'anthropic', 'gemini', 'deepseek', 'custom']) {
const id = (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider);
if (id) {
ids.add(id);
}
}
for (const id of ids) {
try {
const unsubscribe = await adapter_core_1.Credentials.subscribeCredentials(this, id, (changedId, cred) => {
if (cred) {
this.aiCredentialCache.set(changedId, (cred.values?.key || '').trim());
this.log.debug(`AI credential "${changedId}" updated`);
}
else {
// The credential was deleted
this.aiCredentialCache.delete(changedId);
this.log.debug(`AI credential "${changedId}" was deleted`);
}
});
this.aiCredentialUnsubscribers.push(unsubscribe);
// Prime the cache with the current value (the handler may only fire on later changes).
this.aiCredentialCache.set(id, await this.readAiCredentialKey(id));
}
catch (e) {
this.log.warn(`Cannot subscribe to AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
}
}
if (this.aiCredentialUnsubscribers.length) {
this.log.debug(`Subscribed to ${this.aiCredentialUnsubscribers.length} AI credential(s)`);
}
}
/** Tear down all AI credential subscriptions and clear the cache. */
async unsubscribeAiCredentials() {
const unsubscribers = this.aiCredentialUnsubscribers;
this.aiCredentialUnsubscribers = [];
this.aiCredentialCache.clear();
for (const unsubscribe of unsubscribers) {
try {
await unsubscribe();
}
catch (e) {
this.log.warn(`Cannot unsubscribe from AI credential: ${e instanceof Error ? e.message : String(e)}`);
}
}
}
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;
if (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) => {
if (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.su