diff --git a/eslint.config.mjs b/eslint.config.mjs index 82ac6c66e..ef783d59d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,14 +6,23 @@ export default [ languageOptions: { parserOptions: { projectService: { - allowDefaultProject: ['eslint.config.mjs'], + allowDefaultProject: ['*.mjs'], }, tsconfigRootDir: import.meta.dirname, + project: './tsconfig.json', }, }, }, { - ignores: ['src-admin/**/*', 'src/**/*', 'admin/**/*'], + ignores: [ + 'src-admin/**/*', + 'admin/**/*', + 'admin-config/**/*', + 'detection/*', + 'lib/**/*', + 'node_modules/**/*', + 'test/**/*', + ], }, { // disable temporary the rule 'jsdoc/require-param' and enable 'jsdoc/require-jsdoc' diff --git a/install/installTypings.js b/install/installTypings.js index bbec332d7..691e07dbd 100644 --- a/install/installTypings.js +++ b/install/installTypings.js @@ -1,6 +1,6 @@ 'use strict'; -// The adapter includes typings for the lowest supported NodeJS version. +// The adapter includes typings for the lowest supported Node.js version. // This script updates them to the installed version const { spawn } = require('node:child_process'); @@ -16,29 +16,6 @@ function fail(reason) { // Find the latest version on npm console.log('Installing NodeJS typings...'); -npmCommand('view', ['@types/node', 'version'], {stdout: 'pipe', stderr: 'pipe'}) - .then(cmdResult => { - if (cmdResult.exitCode !== 0) { - return fail(cmdResult.stderr); - } - const latestVersion = semver.coerce(cmdResult.stdout).major; - console.log(`latest @types: ${latestVersion}, installed node: ${installedNodeVersion}`); - return semver.gt(`${installedNodeVersion}.0.0`, `${latestVersion}.0.0`) - ? 'latest' // The installed version is too new, install latest - : installedNodeVersion.toString() - ; - }) - .then(targetVersion => { - // Install the desired version - return npmCommand('i', [`@types/node@${targetVersion}`], {stdout: 'ignore', stderr: 'pipe'}); - }) - .then(cmdResult => { - if (cmdResult.exitCode !== 0) { - return fail(cmdResult.stderr); - } else { - process.exit(0); - } - }); // TODO: the following code is copied from a js-controller fork // It should be moved to the core and referenced from there in a future version @@ -60,7 +37,7 @@ npmCommand('view', ['@types/node', 'version'], {stdout: 'pipe', stderr: 'pipe'}) */ /** - * Executes an npm command (e.g. install) and returns the exit code and (if requested) the stdout + * Executes a npm command (e.g., install) and returns the exit code and (if requested) the stdout * @param {string} command The npm command to execute * @param {string[]} [npmArgs] The command line arguments for the npm command * @param {Partial} [options] (optional) Some options for the command execution @@ -78,50 +55,39 @@ function npmCommand(command, npmArgs, options) { const npmBinary = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; /** @type {import("child_process").SpawnOptions} */ const spawnOptions = { - stdio: [ - options.stdin || process.stdin, - options.stdout || process.stdout, - options.stderr || process.stderr, - ], + stdio: [options.stdin || process.stdin, options.stdout || process.stdout, options.stderr || process.stderr], // @ts-ignore This option exists starting with NodeJS 8 windowsHide: true, }; if (options.cwd != null) spawnOptions.cwd = options.cwd; // Now execute the npm process and avoid throwing errors - return new Promise((resolve) => { + return new Promise(resolve => { try { /** @type {string} */ let bufferedStdout; /** @type {string} */ let bufferedStderr; - const cmd = spawn(npmBinary, [command].concat(npmArgs), spawnOptions) - .on('close', (code, signal) => { - resolve({ - exitCode: code, - signal, - stdout: bufferedStdout, - stderr: bufferedStderr - }); + const cmd = spawn(npmBinary, [command].concat(npmArgs), spawnOptions).on('close', (code, signal) => { + resolve({ + exitCode: code, + signal, + stdout: bufferedStdout, + stderr: bufferedStderr, }); + }); // Capture stdout/stderr if requested if (options.stdout === 'pipe') { bufferedStdout = ''; cmd.stdout.on('data', chunk => { - const buffer = Buffer.isBuffer(chunk) - ? chunk - : new Buffer(chunk, 'utf8') - ; + const buffer = Buffer.isBuffer(chunk) ? chunk : new Buffer(chunk, 'utf8'); bufferedStdout += buffer; }); } if (options.stderr === 'pipe') { bufferedStderr = ''; cmd.stderr.on('data', chunk => { - const buffer = Buffer.isBuffer(chunk) - ? chunk - : new Buffer(chunk, 'utf8') - ; + const buffer = Buffer.isBuffer(chunk) ? chunk : new Buffer(chunk, 'utf8'); bufferedStderr += buffer; }); } @@ -130,3 +96,24 @@ function npmCommand(command, npmArgs, options) { } }); } + +npmCommand('view', ['@types/node', 'version'], { stdout: 'pipe', stderr: 'pipe' }) + .then(cmdResult => { + if (cmdResult.exitCode !== 0) { + return fail(cmdResult.stderr); + } + const latestVersion = semver.coerce(cmdResult.stdout).major; + console.log(`latest @types: ${latestVersion}, installed node: ${installedNodeVersion}`); + return semver.gt(`${installedNodeVersion}.0.0`, `${latestVersion}.0.0`) + ? 'latest' // The installed version is too new, install latest + : installedNodeVersion.toString(); + }) + // Install the desired version + .then(targetVersion => npmCommand('i', [`@types/node@${targetVersion}`], { stdout: 'ignore', stderr: 'pipe' })) + .then(cmdResult => { + if (cmdResult.exitCode !== 0) { + fail(cmdResult.stderr); + } else { + process.exit(0); + } + }); diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 78ea00657..6c8d6c92d 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -327,7 +327,21 @@ export const monthShort: Record = { 'zh-cn': ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], }; -export type AstroEvent = 'sunrise' | 'sunset' | 'sunriseEnd' | 'sunsetStart' | 'dawn' | 'dusk' | 'nauticalDawn' | 'nauticalDusk' | 'nadir' | 'nightEnd' | 'night' | 'goldenHourEnd' | 'goldenHour' | 'solarNoon'; +export type AstroEvent = + | 'sunrise' + | 'sunset' + | 'sunriseEnd' + | 'sunsetStart' + | 'dawn' + | 'dusk' + | 'nauticalDawn' + | 'nauticalDusk' + | 'nadir' + | 'nightEnd' + | 'night' + | 'goldenHourEnd' + | 'goldenHour' + | 'solarNoon'; export const astroList: AstroEvent[] = [ 'sunrise', diff --git a/src/lib/convert.ts b/src/lib/convert.ts index 3bc566bdc..c15e0cdd7 100644 --- a/src/lib/convert.ts +++ b/src/lib/convert.ts @@ -1,6 +1,9 @@ // controller uses this file when build uploads -export function stringify(data: { data: ioBroker.ScriptObject | ioBroker.ChannelObject, id: string }): { id: string; data: string | undefined } { - let obj = data.data; +export function stringify(data: { data: ioBroker.ScriptObject | ioBroker.ChannelObject; id: string }): { + id: string; + data: string | undefined; +} { + const obj = data.data; let id = data.id; let result: string | undefined; if (data.data.type === 'channel') { @@ -39,7 +42,10 @@ export function stringify(data: { data: ioBroker.ScriptObject | ioBroker.Channel return { id, data: result }; } -export function parse(data: { data: string, id: string }): { id: string; data: ioBroker.ScriptObject; error: string | undefined } | null { +export function parse(data: { + data: string; + id: string; +}): { id: string; data: ioBroker.ScriptObject; error: string | undefined } | null { let obj: string = data.data; let id = data.id; let error: string | undefined; @@ -58,8 +64,8 @@ export function parse(data: { data: string, id: string }): { id: string; data: i try { result = JSON.parse(obj); - } catch (e) { - error = `Cannot parse object "${name}": ${e}`; + } catch (err: unknown) { + error = `Cannot parse object "${name}": ${err as Error}`; result = { common: { name: name.split('.').pop() || name, @@ -92,8 +98,8 @@ export function parse(data: { data: string, id: string }): { id: string; data: i result = {} as ioBroker.Object; result.common = JSON.parse(stringObj); result.common.source = source; - } catch (e) { - error = `Cannot parse object "${id}": ${e}`; + } catch (err: unknown) { + error = `Cannot parse object "${id}": ${err as Error}`; } } else { source = obj; diff --git a/src/lib/debug.ts b/src/lib/debug.ts index 7d168aac7..46518ddca 100644 --- a/src/lib/debug.ts +++ b/src/lib/debug.ts @@ -1,4 +1,5 @@ import { fork, type ForkOptions } from 'node:child_process'; +import type { DebugState } from '../types'; const adapter = { log: { @@ -7,7 +8,7 @@ const adapter = { warn: (text: string) => console.warn(text), debug: (text: string) => console.log(text), }, - setState: (id:string, val: any): void => { + setState: (id: string, val: any): void => { try { val = JSON.parse(val); } catch (e) { @@ -18,7 +19,7 @@ const adapter = { extendForeignObjectAsync: (id: string, obj: Partial) => { console.log(`EXTEND: ${id} ${JSON.stringify(obj)}`); return Promise.resolve(); - } + }, }; const context: { objects: Record; @@ -26,15 +27,7 @@ const context: { objects: {}, }; -const debugState: { - scriptName: string; - child: any; - promiseOnEnd: any; - paused: boolean; - endTimeout: NodeJS.Timeout | null; - running: boolean; - breakOnStart: boolean; -} = { +const debugState: DebugState = { scriptName: '', child: null, promiseOnEnd: null, @@ -42,32 +35,38 @@ const debugState: { endTimeout: null, running: false, breakOnStart: false, + started: 0, }; -function stopDebug() { +function stopDebug(): Promise { if (debugState.child) { sendToInspector({ cmd: 'end' }); debugState.endTimeout = setTimeout(() => { debugState.endTimeout = null; - debugState.child.kill('SIGTERM'); + debugState.child?.kill('SIGTERM'); }); + debugState.promiseOnEnd = debugState.promiseOnEnd || Promise.resolve(0); } else { - debugState.promiseOnEnd = Promise.resolve(); + debugState.promiseOnEnd = Promise.resolve(0); } return debugState.promiseOnEnd.then(() => { debugState.child = null; debugState.running = false; debugState.scriptName = ''; - debugState.endTimeout && clearTimeout(debugState.endTimeout); - debugState.endTimeout = null; + if (debugState.endTimeout) { + clearTimeout(debugState.endTimeout); + debugState.endTimeout = null; + } }); } -function disableScript(id) { +function disableScript(id: string): Promise { const obj = context.objects[id]; if (obj?.common?.enabled) { - return adapter.extendForeignObjectAsync(obj._id, { common: { enabled: false } } as Partial); + return adapter.extendForeignObjectAsync(obj._id, { + common: { enabled: false }, + } as Partial); } return Promise.resolve(); } @@ -76,8 +75,8 @@ function sendToInspector(message: string | Record): void { if (typeof message === 'string') { try { message = JSON.parse(message); - } catch (e) { - adapter.log.error(`Cannot parse message to inspector: ${message}`); + } catch { + adapter.log.error(`Cannot parse message to inspector: ${JSON.stringify(message)}`); return adapter.setState('debug.from', JSON.stringify({ error: 'Cannot parse message to inspector' })); } } @@ -89,7 +88,7 @@ function sendToInspector(message: string | Record): void { return adapter.setState('debug.from', JSON.stringify({ error: `Cannot send command to terminated inspector` })); } } - +/* function childPrint(text: string): void { console.log( text @@ -100,8 +99,9 @@ function childPrint(text: string): void { .join('\n'), ); } +*/ -function debugScript(data): Promise { +function debugScript(data: { breakOnStart?: boolean; scriptName: string }): Promise { // stop a script if it is running return disableScript(data.scriptName) @@ -133,7 +133,7 @@ function debugScript(data): Promise { }; try { debugMessage = JSON.parse(message); - } catch (e) { + } catch { return adapter.log.error(`Cannot parse message from inspector: ${message}`); } @@ -141,7 +141,7 @@ function debugScript(data): Promise { switch (debugMessage.cmd) { case 'ready': { - debugState.child.send(JSON.stringify({ cmd: 'start', scriptName: debugState.scriptName })); + debugState.child?.send(JSON.stringify({ cmd: 'start', scriptName: debugState.scriptName })); break; } @@ -168,7 +168,9 @@ function debugScript(data): Promise { } case 'readyToDebug': { - console.log(`readyToDebug (set breakpoints): [${debugMessage.scriptId}] ${debugMessage.script}`); + console.log( + `readyToDebug (set breakpoints): [${debugMessage.scriptId}] ${debugMessage.script}`, + ); break; } } diff --git a/src/lib/debugger.ts b/src/lib/debugger.ts index fc4f1ac43..5cefcf811 100644 --- a/src/lib/debugger.ts +++ b/src/lib/debugger.ts @@ -20,12 +20,12 @@ * IN THE SOFTWARE. */ import { join, resolve } from 'node:path'; -import { ReplOptions, REPLServer, start } from 'repl'; +import { type ReplOptions, type REPLServer, start } from 'repl'; import { debuglog as debuglogUtil, type InspectOptions, inspect as inspectUtil } from 'node:util'; -import { Context, runInContext } from 'vm'; +import { type Context, runInContext } from 'node:vm'; import { fileURLToPath } from 'node:url'; -import { builtinModules as PUBLIC_BUILTINS } from 'module'; -import { Debugger, Runtime } from 'node:inspector'; +import { builtinModules as PUBLIC_BUILTINS } from 'node:module'; +import type { Debugger, Runtime } from 'node:inspector'; import { type NodeInspector } from './inspect'; const debuglog = debuglogUtil('inspect'); @@ -93,7 +93,9 @@ const NATIVES = PUBLIC_BUILTINS ? process.binding('natives') : {}; function isNativeUrl(url: string): boolean { url = url.replace(/\.js$/, ''); if (PUBLIC_BUILTINS) { - if (url.startsWith('internal/') || PUBLIC_BUILTINS.includes(url)) return true; + if (url.startsWith('internal/') || PUBLIC_BUILTINS.includes(url)) { + return true; + } } return url in NATIVES || url === 'bootstrap_node'; @@ -182,8 +184,8 @@ class RemoteObject implements Runtime.RemoteObject { } } - [inspectUtil.custom](_depth: number, opts) { - function formatProperty(prop) { + [inspectUtil.custom](_depth: number, opts): string { + function formatProperty(prop): any { switch (prop.type) { case 'string': case 'undefined': @@ -207,6 +209,7 @@ class RemoteObject implements Runtime.RemoteObject { return prop.value; } } + switch (this.type) { case 'boolean': case 'number': @@ -240,7 +243,9 @@ class RemoteObject implements Runtime.RemoteObject { if (this.preview) { const props = this.preview.properties.map((prop, idx) => { const value = formatProperty(prop); - if (prop.name === `${idx}`) return value; + if (prop.name === `${idx}`) { + return value; + } return `${prop.name}: ${value}`; }); if (this.preview.overflow) { @@ -251,10 +256,10 @@ class RemoteObject implements Runtime.RemoteObject { return this.subtype === 'array' ? `[ ${propString} ]` : `{ ${propString} }`; } - return this.description; + return this.description || ''; default: - return this.description; + return this.description || ''; } } @@ -306,14 +311,14 @@ class ScopeSnapshot implements Debugger.Scope { this.completionGroup = properties.map(prop => prop.name); } - [inspectUtil.custom](_depth: number, opts: InspectOptions) { + [inspectUtil.custom](_depth: number, opts: InspectOptions): string { const type = `${this.type[0].toUpperCase()}${this.type.slice(1)}`; const name = this.name ? `<${this.name}>` : ''; const prefix = `${type}${name} `; return inspectUtil(this.properties, opts).replace(/^Map /, prefix); } } - +/* function copyOwnProperties(target: any, source: any): void { Object.getOwnPropertyNames(source).forEach(prop => { const descriptor = Object.getOwnPropertyDescriptor(source, prop); @@ -322,7 +327,7 @@ function copyOwnProperties(target: any, source: any): void { } }); } -/* + function aliasProperties(target, mapping) { Object.keys(mapping).forEach((key) => { const descriptor = Object.getOwnPropertyDescriptor(target, key); @@ -333,7 +338,7 @@ function aliasProperties(target, mapping) { export default function createRepl(inspector: NodeInspector): () => REPLServer { const { Debugger, /*HeapProfiler, Profiler,*/ Runtime } = inspector; - let repl: REPLServer; // eslint-disable-line prefer-const + let repl: REPLServer; // Things we want to keep around // const history = { control: [], debug: [] }; @@ -343,45 +348,50 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { let lastCommand: string; // Things we need to reset when the app restarts - let knownScripts: Record; - let currentBacktrace; - let selectedFrame; - let exitDebugRepl; + let knownScripts: Record = {}; + let currentBacktrace: CallFrame[] | undefined; + let selectedFrame: CallFrame | undefined; + let exitDebugRepl: undefined | (() => void); - function resetOnStart() { + function resetOnStart(): void { knownScripts = {}; - currentBacktrace = null; - selectedFrame = null; + currentBacktrace = undefined; + selectedFrame = undefined; - if (exitDebugRepl) exitDebugRepl(); - exitDebugRepl = null; + if (exitDebugRepl) { + exitDebugRepl(); + } + exitDebugRepl = undefined; } + resetOnStart(); const INSPECT_OPTIONS: InspectOptions = { colors: inspector.stdout.isTTY }; - function inspect(value) { + function inspect(value: any): string { return inspectUtil(value, INSPECT_OPTIONS); } - function print(value, oneline = false) { + function print(value: any, oneline = false): void { const text = typeof value === 'string' ? value : inspect(value); return inspector.print(text, oneline); } - function getCurrentLocation() { + function getCurrentLocation(): Debugger.Location { if (!selectedFrame) { throw new Error('Requires execution to be paused'); } return selectedFrame.location; } - function isCurrentScript(script) { - return selectedFrame && getCurrentLocation().scriptId === script.scriptId; + function isCurrentScript(script): boolean { + return !!selectedFrame && getCurrentLocation().scriptId === script.scriptId; } - function formatScripts(displayNatives = false) { - function isVisible(script) { - if (displayNatives) return true; + function formatScripts(displayNatives = false): string { + function isVisible(script): boolean { + if (displayNatives) { + return true; + } return !script.isNative || isCurrentScript(script); } @@ -396,10 +406,10 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { }) .join('\n'); } - function listScripts(displayNatives = false) { + function listScripts(displayNatives = false): void { print(formatScripts(displayNatives)); } - listScripts[inspectUtil.custom] = function listWithoutInternal() { + listScripts[inspectUtil.custom] = function listWithoutInternal(): string { return formatScripts(); }; /* @@ -452,7 +462,7 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { Object.assign(this, location); } - [inspectUtil.custom](_depth: number, options: InspectOptions) { + [inspectUtil.custom](_depth: number, options: InspectOptions): string { const { /* scriptId, */ lineNumber, columnNumber, delta, scriptSource } = this; const start = Math.max(1, lineNumber - delta + 1); const end = lineNumber + delta + 1; @@ -542,7 +552,7 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { Object.assign(this, callFrame); } - loadScopes() { + loadScopes(): Promise { return Promise.all( this.scopeChain .filter(scope => scope.type !== 'global') @@ -551,11 +561,9 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { return Runtime.getProperties({ objectId, generatePreview: true, - } as Runtime.GetPropertiesParameterType).then( - ({ result }: Runtime.GetPropertiesReturnType) => { - return new ScopeSnapshot(scope, result); - }, - ); + } as Runtime.GetPropertiesParameterType).then(({ result }: Runtime.GetPropertiesReturnType) => { + return new ScopeSnapshot(scope, result); + }); }), ); } @@ -566,7 +574,7 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { } class Backtrace extends Array { - [inspectUtil.custom]() { + [inspectUtil.custom](): string { return this.map((callFrame, idx) => { const { location: { scriptId, lineNumber, columnNumber }, @@ -641,7 +649,7 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { callback: (error: Error | null, result?: any) => void, ): void { debuglog('eval:', input); - function returnToCallback(error: Error | null, result?: any) { + function returnToCallback(error: Error | null, result?: any): void { debuglog('end-eval:', input, error); callback(error, result); } @@ -900,7 +908,10 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { // }); // } - Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }: Debugger.PausedEventDataType) => { + Debugger.on('paused', (data: Debugger.PausedEventDataType): void => { + const callFrames: CallFrame[] = data.callFrames as CallFrame[]; + const reason: string = data.reason; + if (process.env.NODE_INSPECT_RESUME_ON_START === '1' && reason === 'Break on start') { debuglog('Paused on start, but NODE_INSPECT_RESUME_ON_START environment variable is set to 1, resuming'); inspector.client.callMethod('Debugger.resume'); @@ -908,7 +919,7 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { } // Save execution context's data - currentBacktrace = Backtrace.from(callFrames as CallFrame[]); + currentBacktrace = Backtrace.from(callFrames); selectedFrame = currentBacktrace[0]; const { scriptId, lineNumber } = selectedFrame.location; @@ -919,7 +930,7 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { const header = `${breakType} in ${scriptUrl}:${lineNumber + 1}`; inspector.suspendReplWhile(() => - Promise.all([/*formatWatchers(true), */ selectedFrame.list(3)]) + Promise.all([/*formatWatchers(true), */ selectedFrame?.list(3)]) .then(([/*watcherList, */ context]) => { /*if (watcherList) { return `${watcherList}\n${inspect(context)}`; @@ -932,9 +943,9 @@ export default function createRepl(inspector: NodeInspector): () => REPLServer { ); }); - function handleResumed() { - currentBacktrace = null; - selectedFrame = null; + function handleResumed(): void { + currentBacktrace = undefined; + selectedFrame = undefined; } Debugger.on('resumed', handleResumed); diff --git a/src/lib/eventObj.ts b/src/lib/eventObj.ts index 326cd06ee..a241d7e60 100644 --- a/src/lib/eventObj.ts +++ b/src/lib/eventObj.ts @@ -1,4 +1,4 @@ -import { JavascriptContext } from '../types'; +import type { JavascriptContext } from '../types'; let gContext: JavascriptContext; @@ -30,13 +30,11 @@ export function getObjectEnumsSync( } for (let i = 0, l = context.enums.length; i < l; i++) { - if ( - context.objects[context.enums[i]]?.common?.members?.includes(idObj) - ) { + if (context.objects[context.enums[i]]?.common?.members?.includes(idObj)) { if (!enumIds.includes(context.enums[i])) { enumIds.push(context.enums[i]); } - let name: ioBroker.StringOrTranslated = context.objects[context.enums[i]].common.name; + const name: ioBroker.StringOrTranslated = context.objects[context.enums[i]].common.name; const str: string | undefined = typeof name === 'object' ? name[gContext.language || 'en'] : name; if (str && !enumNames.includes(str)) { enumNames.push(str); @@ -71,7 +69,7 @@ export function getObjectEnumsSync( return context.cacheObjectEnums[idObj]; } -function doGetter(obj: Record, name: string, ret: any) { +function doGetter(obj: Record, name: string, ret: any): any { //adapter.log.debug('getter: ' + name + ' returns ' + ret); Object.defineProperty(obj, name, { value: ret }); return ret; @@ -83,38 +81,37 @@ export class EventObj { public newState: ioBroker.State; public oldState: ioBroker.State; - constructor(id: string, state: ioBroker.State, oldState?: ioBroker.State, context?: JavascriptContext) { + constructor( + id: string, + state: ioBroker.State | null | undefined, + oldState?: ioBroker.State | null, + context?: JavascriptContext, + ) { if (context && !gContext) { gContext = context; } this.id = id; - this.newState = { - val: state.val, - ts: state.ts, - ack: state.ack, - lc: state.lc, - from: state.from, - q: state.q, - c: state.c, - user: state.user, - }; - //if (oldState === undefined) oldState = {}; + if (!state) { + this.newState = { q: undefined, c: undefined, user: undefined } as ioBroker.State; + } else { + this.newState = { + val: state.val, + ts: state.ts, + ack: state.ack, + lc: state.lc, + from: state.from, + q: state.q, + c: state.c, + user: state.user, + }; + } + // if (oldState === undefined) oldState = {}; if (!oldState) { this.oldState = { - // @ts-expect-error we assume it could be - val: undefined, - // @ts-expect-error we assume it could be - ts: undefined, - // @ts-expect-error we assume it could be - ack: undefined, - // @ts-expect-error we assume it could be - lc: undefined, - // @ts-expect-error we assume it could be - from: undefined, q: undefined, c: undefined, user: undefined, - }; + } as ioBroker.State; } else { this.oldState = { val: oldState.val, @@ -133,7 +130,7 @@ export class EventObj { get common(): ioBroker.ObjectCommon { const ret = gContext.objects[this.id] ? gContext.objects[this.id].common : {}; return doGetter(this, 'common', ret); - }; + } get native(): Record { const ret = gContext.objects[this.id] ? gContext.objects[this.id].native : {}; return doGetter(this, 'native', ret); @@ -142,14 +139,13 @@ export class EventObj { const ret = this.common ? this.common.name : null; return doGetter(this, 'name', ret); } - get channelId(): string | null{ + get channelId(): string | null { const ret = this.id.replace(/\.*[^.]+$/, ''); return doGetter(this, 'channelId', gContext.objects[ret] ? ret : null); } - get channelName(): string | null{ + get channelName(): string | null { const channelId = this.channelId; - const ret = - channelId && gContext.objects[channelId].common ? gContext.objects[channelId].common.name : null; + const ret = channelId && gContext.objects[channelId].common ? gContext.objects[channelId].common.name : null; return doGetter(this, 'channelName', ret); } get deviceId(): string | null { @@ -163,8 +159,7 @@ export class EventObj { } get deviceName(): ioBroker.StringOrTranslated | null { const deviceId = this.deviceId; - const ret = - deviceId && gContext.objects[deviceId].common ? gContext.objects[deviceId].common.name : null; + const ret = deviceId && gContext.objects[deviceId].common ? gContext.objects[deviceId].common.name : null; return doGetter(this, 'deviceName', ret); } get enumIds(): string[] | undefined { @@ -192,9 +187,9 @@ export class EventObj { export function createEventObject( context: JavascriptContext, id: string, - state: ioBroker.State, - oldState: ioBroker.State, -) { + state: ioBroker.State | null | undefined, + oldState: ioBroker.State | null | undefined, +): EventObj { gContext = gContext || context; return new EventObj(id, state, oldState, context); diff --git a/src/lib/inspect.ts b/src/lib/inspect.ts index d2e79137e..19d826901 100644 --- a/src/lib/inspect.ts +++ b/src/lib/inspect.ts @@ -25,18 +25,22 @@ import { connect } from 'node:net'; import { debuglog as utilDebugLog, inspect } from 'node:util'; import { normalize, join } from 'node:path'; import { existsSync } from 'node:fs'; -import { WriteStream, ReadStream } from 'node:tty'; +import type { WriteStream, ReadStream } from 'node:tty'; import InspectClient from 'node-inspect/lib/internal/inspect_client'; import createRepl from './debugger'; -import { Debugger, Runtime } from 'node:inspector'; -import { REPLServer } from 'repl'; -import { ChildProcessWithoutNullStreams } from 'child_process'; +import type { Debugger, Runtime } from 'node:inspector'; +import type { REPLServer } from 'repl'; +import type { ChildProcessWithoutNullStreams } from 'child_process'; // const runAsStandalone = typeof __dirname !== 'undefined'; const breakOnStart = process.argv.includes('--breakOnStart'); const debuglog = utilDebugLog('inspect'); +let inspector: NodeInspector; +let scriptToDebug = ''; // script.js.yy +let instanceToDebug = ''; // adapter.X +let alreadyPausedOnFirstLine = false; class StartupError extends Error { constructor(message: string) { @@ -60,14 +64,14 @@ function portIsFree(host: string, port: number, timeout: number = 9999): Promise reject(new StartupError(`Timeout (${timeout}) waiting for ${host}:${port} to be free`)); }, timeout); - function pingPort() { + function pingPort(): void { if (didTimeOut) { return; } const socket = connect(port, host); let didRetry = false; - function retry() { + function retry(): void { if (!didRetry && !didTimeOut) { didRetry = true; setTimeout(pingPort, retryDelay); @@ -450,7 +454,7 @@ export class NodeInspector { ); } - killChild() { + killChild(): void { this.client.reset(); if (this.child) { this.child.kill(); @@ -458,14 +462,14 @@ export class NodeInspector { } } - run() { + run(): Promise { this.killChild(); return this._runScript().then(([child, port, host]) => { this.child = child; let connectionAttempts = 0; - const attemptConnect = () => { + const attemptConnect = (): void => { ++connectionAttempts; debuglog('connection attempt #%d', connectionAttempts); this.stdout.write('.'); @@ -474,7 +478,7 @@ export class NodeInspector { debuglog('connection established'); this.stdout.write(' ok'); }, - (error: any) => { + (error: unknown): Promise => { debuglog('connect failed', error); // If it's failed to connect 10 times, then print a failed message if (connectionAttempts >= 10) { @@ -581,25 +585,24 @@ function startInspect( argv: string[] = process.argv.slice(2), stdin: ReadStream = process.stdin, stdout: WriteStream = process.stdout, -) { - /* eslint-disable no-console */ - /*if (argv.length < 1) { - const invokedAs = runAsStandalone ? - 'node-inspect' : - `${process.argv0} ${process.argv[1]}`; +): void { + /* + if (argv.length < 1) { + const invokedAs = runAsStandalone ? 'node-inspect' : `${process.argv0} ${process.argv[1]}`; console.error(`Usage: ${invokedAs} script.js`); console.error(` ${invokedAs} :`); console.error(` ${invokedAs} -p `); process.exit(1); - }*/ + } + */ const options = parseArgv(argv); inspector = new NodeInspector(options, stdin, stdout); stdin.resume(); - function handleUnexpectedError(e) { + function handleUnexpectedError(e: Error): void { if (!(e instanceof StartupError)) { console.error('There was an internal error in node-inspect. Please report this bug.'); console.error(e.message); @@ -631,17 +634,12 @@ function convertResultToError(result: Runtime.RemoteObject): Error { return err; } -let inspector: NodeInspector; -let scriptToDebug: string; // script.js.yy -let instanceToDebug: string; // adapter.X -let alreadyPausedOnFirstLine = false; - -process.on('message', (message: DebugCommand) => { +process.on('message', (message: DebugCommand): void => { if (typeof message === 'string') { try { message = JSON.parse(message); - } catch (e) { - return console.error(`Cannot parse: ${message}`); + } catch { + return console.error(`Cannot parse: ${JSON.stringify(message)}`); } } processCommand(message); @@ -705,7 +703,7 @@ type DebugCommand = { end?: number; }; -function processCommand(data: DebugCommand) { +function processCommand(data: DebugCommand): void { if (data.cmd === 'start') { scriptToDebug = data.scriptName as string; // we can request the script by name or iobroker instance to debug @@ -735,10 +733,11 @@ function processCommand(data: DebugCommand) { if (!file) { sendToHost({ cmd: 'error', error: `Cannot locate iobroker.${adapter}`, errorContext: e }); - return setTimeout(() => { + setTimeout(() => { sendToHost({ cmd: 'finished', context: `Cannot locate iobroker.${adapter}` }); setTimeout(() => process.exit(124), 500); }, 200); + return; } } file = file.replace(/\\/g, '/'); @@ -750,7 +749,8 @@ function processCommand(data: DebugCommand) { process.exit(); } else if (data.cmd === 'source') { inspector.Debugger.getScriptSource({ scriptId: data.scriptId } as Debugger.GetScriptSourceParameterType).then( - (script: Debugger.GetScriptSourceReturnType) => sendToHost({ cmd: 'script', scriptId: data.scriptId, text: script.scriptSource }), + (script: Debugger.GetScriptSourceReturnType) => + sendToHost({ cmd: 'script', scriptId: data.scriptId, text: script.scriptSource }), ); } else if (data.cmd === 'cont') { inspector.Debugger.resume().catch(e => sendToHost({ cmd: 'error', error: e })); @@ -869,12 +869,16 @@ function processCommand(data: DebugCommand) { ) || [], ).then(expressions => sendToHost({ cmd: 'expressions', expressions })); } else if (data.cmd === 'stopOnException') { - inspector.Debugger.setPauseOnExceptions({ state: data.state ? 'all' : 'none' } as Debugger.SetPauseOnExceptionsParameterType).catch(e => + inspector.Debugger.setPauseOnExceptions({ + state: data.state ? 'all' : 'none', + } as Debugger.SetPauseOnExceptionsParameterType).catch(e => sendToHost({ cmd: 'stopOnException', variableName: `Cannot stopOnException: ${e}`, errorContext: e }), ); } else if (data.cmd === 'getPossibleBreakpoints') { inspector.Debugger.getPossibleBreakpoints({ start: data.start, end: data.end }) - .then((breakpoints: Debugger.GetPossibleBreakpointsReturnType) => sendToHost({ cmd: 'getPossibleBreakpoints', breakpoints: breakpoints.locations })) + .then((breakpoints: Debugger.GetPossibleBreakpointsReturnType) => + sendToHost({ cmd: 'getPossibleBreakpoints', breakpoints: breakpoints.locations }), + ) .catch(e => sendToHost({ cmd: 'getPossibleBreakpoints', @@ -887,11 +891,15 @@ function processCommand(data: DebugCommand) { } } -function sendToHost(data: CommandToHost) { +function sendToHost(data: CommandToHost): void { if (data.cmd === 'error') { console.error(data.text); - data.expr && console.error(`[EXPRESSION] ${data.expr}`); - data.bp && console.error(`[BP] ${data.bp}`); + if (data.expr) { + console.error(`[EXPRESSION] ${JSON.stringify(data.expr)}`); + } + if (data.bp) { + console.error(`[BP] ${JSON.stringify(data.bp)}`); + } } process.send && process.send(JSON.stringify(data)); diff --git a/src/lib/javascript.d.ts b/src/lib/javascript.d.ts index 934374553..e21aab27f 100644 --- a/src/lib/javascript.d.ts +++ b/src/lib/javascript.d.ts @@ -1,6 +1,7 @@ // import all modules that are available in the sandbox // this has a nice side effect that we may augment the global scope -import * as os from 'os'; +import type * as os from 'node:os'; +import type { ChildProcess, ExecException } from 'node:child_process'; type EmptyCallback = () => void | Promise; type ErrorCallback = (err?: Error) => void | Promise; @@ -68,7 +69,6 @@ declare global { enum StateQuality { good = 0x00, // or undefined or null bad = 0x01, - general_problem = 0x01, general_device_problem = 0x41, general_sensor_problem = 0x81, device_not_connected = 0x42, @@ -83,7 +83,7 @@ declare global { val: T; } - interface AbsentState extends ioBroker.State { + interface AbsentState extends Omit { val: null; notExist: true; @@ -227,8 +227,7 @@ declare global { type ObjectIdToObjectType< T extends string, Read extends 'read' | 'write' = 'read', - O = // State must come before Adapter or system.adapter.admin.0.foobar will resolve to AdapterObject - T extends ObjectIDs.State + O = T extends ObjectIDs.State // State must come before Adapter or system.adapter.admin.0.foobar will resolve to AdapterObject ? StateObject : // Instance and Adapter must come before meta or `system.adapter.admin` will resolve to MetaObject T extends ObjectIDs.Instance @@ -308,7 +307,7 @@ declare global { role: string; /** the default value */ - def?: any; + def?: ioBroker.StateValue; /** the default status of the ack flag */ defAck?: boolean; @@ -432,199 +431,13 @@ declare global { }; } - interface UserCommon extends ObjectCommon { - /** The username */ - name: string; - /** The hashed password */ - password: string; - /** Whether this user is enabled */ - enabled: boolean; - - // Make it possible to narrow the object type using the custom property - custom?: undefined; - } + type UserCommon = ioBroker.UserCommon; - interface GroupCommon extends ObjectCommon { - /** The name of this group */ - name: string; - /** The users of this group */ - members: string[]; // system.user.name, ... - /** The default permissions of this group */ - acl: Omit; + type GroupCommon = ioBroker.GroupCommon; - // Make it possible to narrow the object type using the custom property - custom?: undefined; - } - - interface ScriptCommon extends ObjectCommon { - name: string; - /** Defines the type of the script, e.g. TypeScript/ts, JavaScript/js or Blockly */ - engineType: string; - /** The instance id of the instance which executes this script */ - engine: string; - /** The source code of this script */ - source: string; - debug: boolean; - verbose: boolean; - /** Whether this script should be executed */ - enabled: boolean; - /** Is used to determine whether a script has changed and needs to be recompiled */ - sourceHash?: string; - /** If the script uses a compiled language like TypeScript, this contains the compilation output */ - compiled?: string; - /** If the script uses a compiled language like TypeScript, this contains the generated declarations (global scripts only) */ - declarations?: string; - - // Make it possible to narrow the object type using the custom property - custom?: undefined; - } - - type WelcomeScreenEntry = - | string - | { - link: string; - name: string; - img: string; - color: string; - }; - - interface AdapterCommon extends ObjectCommon { - /** Custom attributes to be shown in admin in the object browser */ - adminColumns?: any[]; - /** Settings for custom Admin Tabs */ - adminTab?: { - name?: string; - /** Icon name for FontAwesome */ - 'fa-icon'?: string; - /** If true, the Tab is not reloaded when the configuration changes */ - ignoreConfigUpdate?: boolean; - /** Which URL should be loaded in the tab. Supports placeholders like http://%ip%:%port% */ - link?: string; - /** If true, only one instance of this tab will be created for all instances */ - singleton?: boolean; - }; - allowInit?: boolean; - /** Possible values for the instance mode (if more than one is possible) */ - availableModes?: InstanceMode[]; - /** Whether this adapter includes custom blocks for Blockly. If true, `admin/blockly.js` must exist. */ - blockly?: boolean; - /** Where the adapter will get its data from. Set this together with @see dataSource */ - connectionType?: 'local' | 'cloud'; - /** If true, this adapter can be started in compact mode (in the same process as other adpaters) */ - compact?: boolean; - /** The directory relative to iobroker-data where the adapter stores the data. Supports the placeholder `%INSTANCE%`. This folder will be backed up and restored automatically. */ - dataFolder?: string; - /** How the adapter will mainly receive its data. Set this together with @see connectionType */ - dataSource?: 'poll' | 'push' | 'assumption'; - /** A record of ioBroker adapters (including "js-controller") and version ranges which are required for this adapter. */ - dependencies?: Array>; - /** Which files outside of the README.md have documentation for the adapter */ - docs?: Partial>; - /** Whether new instances should be enabled by default. *Should* be `false`! */ - enabled: boolean; - /** If true, all previous data in the target directory (web) should be deleted before uploading */ - eraseOnUpload?: boolean; - /** URL of an external icon that is shown for adapters that are not installed */ - extIcon?: string; - /** Whether this adapter responds to `getHistory` messages */ - getHistory?: boolean; - /** Filename of the local icon which is shown for installed adapters. Should be located in the `admin` directory */ - icon?: string; - /** Which version of this adapter is installed */ - installedVersion: string; - keywords?: string[]; - /** A dictionary of links to web services this adapter provides */ - localLinks?: Record; - /** @deprecated Use @see localLinks */ - localLink?: string; - logLevel?: LogLevel; - /** Whether this adapter receives logs from other hosts and adapters (e.g., to strore them somewhere) */ - logTransporter?: boolean; - /** Path to the start file of the adapter. Should be the same as in `package.json` */ - main?: string; - /** Whether the admin tab is written in materialize style. Required for Admin 3+ */ - materializeTab: boolean; - /** Whether the admin configuration dialog is written in materialize style. Required for Admin 3+ */ - materialize: boolean; - /** If `true`, the object `system.adapter...messagebox will be created to send messages to the adapter (used for email, pushover, etc...) */ - messagebox?: true; - mode: InstanceMode; - /** Name of the adapter (without leading `ioBroker.`) */ - name: string; - /** If `true`, no configuration dialog will be shown */ - noConfig?: true; - /** If `true`, this adapter's instances will not be shown in the admin overview screen. Useful for icon sets and widgets... */ - noIntro?: true; - /** Set to `true` if the adapter is not available in the official ioBroker repositories. */ - noRepository?: true; - /** If `true`, manual installation from GitHub is not possible */ - nogit?: true; - /** If `true`, this adapter cannot be deleted or updated manually. */ - nondeletable?: true; - /** If `true`, this "adapter" only contains HTML files and no main executable */ - onlyWWW?: boolean; - /** Used to configure native (OS) dependencies of this adapter that need to be installed with system package manager before installing the adapter */ - osDependencies?: { - /** For OSX */ - darwin: string[]; - /** For Linux */ - linux: string[]; - /** For Windows */ - win32: string[]; - }; - /** Which OSes this adapter supports */ - os?: 'linux' | 'darwin' | 'win32' | Array<'linux' | 'darwin' | 'win32'>; - platform: 'Javascript/Node.js'; - /** The keys of common attributes (e.g. `history`) which are not deleted in a `setObject` call even if they are not present. Deletion must be done explicitly by setting them to `null`. */ - preserveSettings?: string | string[]; - /** Which adapters must be restarted after installing or updating this adapter. */ - restartAdapters?: string[]; - /** If the adapter runs in `schedule` mode, this contains the CRON */ - schedule?: string; - serviceStates?: boolean | string; - /** Whether this adapter may only be installed once per host */ - singletonHost?: boolean; - /** Whether this adapter may only be installed once in the whole system */ - singleton?: boolean; - /** Whether the adapter must be stopped before an update */ - stopBeforeUpdate?: boolean; - /** Overrides the default timeout that ioBroker will wait before force-stopping the adapter */ - stopTimeout?: number; - subscribable?: boolean; - subscribe?: any; // ? - /** If `true`, this adapter provides custom per-state settings. Requires a `custom_m.html` file in the `admin` directory. */ - supportCustoms?: boolean; - /** Whether the adapter supports the signal stopInstance via messagebox */ - supportStopInstance?: boolean; - /** The translated names of this adapter to be shown in the admin UI */ - titleLang?: Record; - /** @deprecated The name of this adapter to be shown in the admin UI. Use @see titleLang instead. */ - title?: string; - /** The type of this adapter */ - type?: string; - /** If `true`, the `npm` package must be installed with the `--unsafe-perm` flag */ - unsafePerm?: true; - /** The available version in the ioBroker repo. */ - version: string; - /** If `true`, the adapter will be started if any value is written into `system.adapter...wakeup. Normally, the adapter should stop after processing the event. */ - wakeup?: boolean; - /** Include the adapter version in the URL of the web adapter, e.g. `http://ip:port/1.2.3/material` instead of `http://ip:port/material` */ - webByVersion?: boolean; - /** Whether the web server in this adapter can be extended with plugin/extensions */ - webExtendable?: boolean; - /** Relative path to a module that contains an extension for the web adapter. Use together with @see native.webInstance to configure which instances this affects */ - webExtension?: string; - webPreSettings?: any; // ? - webservers?: any; // ? - /** A list of pages that should be shown on the "web" index page */ - welcomeScreen?: WelcomeScreenEntry[]; - /** A list of pages that should be shown on the ioBroker cloud index page */ - welcomeScreenPro?: WelcomeScreenEntry[]; - wwwDontUpload?: boolean; + type ScriptCommon = ioBroker.ScriptCommon; - // Make it possible to narrow the object type using the custom property - custom?: undefined; - } + type AdapterCommon = ioBroker.AdapterCommon; interface OtherCommon extends ObjectCommon { [propName: string]: any; @@ -833,7 +646,8 @@ declare global { type SettableOtherObject = SettableObject; /** Represents the change of a state */ - interface ChangedStateObject extends StateObject { + interface ChangedStateObject + extends StateObject { common: StateCommon; native: Record; id?: string; @@ -866,7 +680,7 @@ declare global { err?: Error | null, state?: TypedState | AbsentState, ) => void | Promise; - type ExistsStateCallback = (err?: Error | null, exists?: Boolean) => void | Promise; + type ExistsStateCallback = (err?: Error | null, exists?: boolean) => void | Promise; type SetStateCallback = (err?: Error | null, id?: string) => void | Promise; type SetStatePromise = Promise>; @@ -915,11 +729,7 @@ declare global { type LogLevel = 'silly' | 'debug' | 'info' | 'warn' | 'error' | 'force'; - type ReadFileCallback = ( - err?: Error | null, - file?: Buffer | string, - mimeType?: string, - ) => void | Promise; + type ReadFileCallback = (err?: Error | null, file?: Buffer | string, mimeType?: string) => void | Promise; type ReadFilePromise = Promise>; /** Callback information for a passed message */ @@ -1070,19 +880,28 @@ declare global { */ getState(callback: GetStateCallback): void; getState(): TypedState | null | undefined; - getStateAsync(): Promise | null | undefined>; + getStateAsync(): Promise< + TypedState | iobJS.AbsentState | null | undefined + >; /** * Sets all queried states to the given value. */ - setState(state: ioBroker.StateValue | ioBroker.SettableState, ack?: boolean | 'true' | 'false' | SetStateCallback, callback?: SetStateCallback): this; - setStateAsync(state: ioBroker.StateValue | ioBroker.SettableState, ack?: boolean | 'true' | 'false'): Promise; + setState( + state: ioBroker.StateValue | ioBroker.SettableState, + ack?: boolean | 'true' | 'false' | SetStateCallback, + callback?: SetStateCallback, + ): this; + setStateAsync( + state: ioBroker.StateValue | ioBroker.SettableState, + ack?: boolean | 'true' | 'false', + ): Promise; setStateDelayed( state: any, - isAck?: boolean, - delay?: number, - clearRunning?: boolean, - callback?: SetStateCallback, + isAck: boolean | number | undefined, + delay?: number | boolean, + clearRunning?: boolean | (() => void), + callback?: () => void, ): this; /** @@ -1102,20 +921,20 @@ declare global { } /** - * * "sunrise": sunrise (top edge of the sun appears on the horizon) - * * "sunriseEnd": sunrise ends (bottom edge of the sun touches the horizon) - * * "goldenHourEnd": morning golden hour (soft light, best time for photography) ends - * * "solarNoon": solar noon (sun is in the highest position) - * * "goldenHour": evening golden hour starts - * * "sunsetStart": sunset starts (bottom edge of the sun touches the horizon) - * * "sunset": sunset (sun disappears below the horizon, evening civil twilight starts) - * * "dusk": dusk (evening nautical twilight starts) - * * "nauticalDusk": nautical dusk (evening astronomical twilight starts) - * * "night": night starts (dark enough for astronomical observations) - * * "nightEnd": night ends (morning astronomical twilight starts) - * * "nauticalDawn": nautical dawn (morning nautical twilight starts) - * * "dawn": dawn (morning nautical twilight ends, morning civil twilight starts) - * * "nadir": nadir (darkest moment of the night, sun is in the lowest position) + * - "sunrise": sunrise (top edge of the sun appears on the horizon) + * - "sunriseEnd": sunrise ends (bottom edge of the sun touches the horizon) + * - "goldenHourEnd": morning golden hour (soft light, best time for photography) ends + * - "solarNoon": solar noon (sun is in the highest position) + * - "goldenHour": evening golden hour starts + * - "sunsetStart": sunset starts (bottom edge of the sun touches the horizon) + * - "sunset": sunset (sun disappears below the horizon, evening civil twilight starts) + * - "dusk": dusk (evening nautical twilight starts) + * - "nauticalDusk": nautical dusk (evening astronomical twilight starts) + * - "night": night starts (dark enough for astronomical observations) + * - "nightEnd": night ends (morning astronomical twilight starts) + * - "nauticalDawn": nautical dawn (morning nautical twilight starts) + * - "dawn": dawn (morning nautical twilight ends, morning civil twilight starts) + * - "nadir": nadir (darkest moment of the night, sun is in the lowest position) */ type AstroPattern = | 'sunrise' @@ -1292,7 +1111,7 @@ declare global { /** * The name of the current script */ - // @ts-ignore We need this variable, although it conflicts with lib.es6 + // @ts-expect-error We need this variable, although it conflicts with lib.es6 const name: string; /** * The name of the current script @@ -1311,20 +1130,21 @@ declare global { /** * Queries all states with the given selector + * * @param selector See @link{https://github.com/ioBroker/ioBroker.javascript#---selector} for a description */ - // @ts-ignore We need this function, although it conflicts with vue function $(selector: string): iobJS.QueryResult; /** * Prints a message in the ioBroker log + * * @param message The message to print * @param severity (optional) severity of the message. default = "info" */ function log(message: any, severity?: iobJS.LogLevel): void; // console functions - // @ts-ignore We need this variable, although it conflicts with the node typings + // @ts-expect-error We need this variable, although it conflicts with the node typings namespace console { /** log a message with info level */ function log(message: any): void; @@ -1341,7 +1161,10 @@ declare global { /** * Executes a system command */ - const exec: typeof import('child_process').exec; + const exec: ( + command: string, + callback?: (error: ExecException | null, stdout: string, stderr: string) => void, + ) => ChildProcess; /** * Sends an email using the email adapter. @@ -1410,6 +1233,7 @@ declare global { /** * Subscribe to the changes of the matched files. * The return value can be used for offFile later + * * @param id ID of meta-object, like `vis.0` * @param filePattern File name or file pattern, like `main/*` * @param withFile If the content of the file must be returned in callback (high usage of memory) @@ -1425,10 +1249,11 @@ declare global { /** * Un-subscribe from the changes of the matched files. - * @param id ID of meta-object, like `vis.0`. You can provide here can be a returned object from onFile. In this case, no filePattern required. + * + * @param id ID of a meta-object, like `vis.0`. You can provide here can be a returned object from onFile. In this case, no filePattern required. * @param filePattern File name or file pattern, like `main/*` */ - function offFile(id: string | any, filePattern?: string | string[]): boolean; + function offFile(id: string | string[], filePattern?: string | string[]): boolean; /** * Registers a one-time subscription which automatically unsubscribes after the first invocation @@ -1445,26 +1270,30 @@ declare global { * Causes all changes of the state with id1 to the state with id2. * The return value can be used to unsubscribe later */ - function on(id1: string, id2: string): any; + function on(id1: string, id2: string): void; + /** * Watches the state with id1 for changes and overwrites the state with id2 with value2 when any occur. + * * @param id1 The state to watch for changes * @param id2 The state to update when changes occur * @param value2 The value to write into state `id2` when `id1` gets changed */ - function on(id1: string, id2: string, value2: any): any; + function on(id1: string, id2: string, value2: any): void; /** * Causes all changes of the state with id1 to the state with id2 */ - function subscribe(id1: string, id2: string): any; + function subscribe(id1: string, id2: string): void; + /** * Watches the state with id1 for changes and overwrites the state with id2 with value2 when any occur. + * * @param id1 The state to watch for changes * @param id2 The state to update when changes occur * @param value2 The value to write into state `id2` when `id1` gets changed */ - function subscribe(id1: string, id2: string, value2: any): any; + function subscribe(id1: string, id2: string, value2: any): void; /** * Returns the list of all active subscriptions @@ -1479,8 +1308,7 @@ declare global { /** * Unsubscribe from changes of the given object ID(s) or handler(s) */ - function unsubscribe(id: string | string[]): boolean; - function unsubscribe(handler: any | any[]): boolean; + function unsubscribe(id: string | RegExp | string[]): boolean; function adapterSubscribe(id: string): void; function adapterUnsubscribe(id: string): void; @@ -1515,6 +1343,7 @@ declare global { /** * Calculates the astro time which corresponds to the given pattern. * For valid patterns, see @link{https://github.com/ioBroker/ioBroker.javascript/blob/master/docs/en/javascript.md#astro-function} + * * @param pattern One of predefined patterns, like: sunrise, sunriseEnd, ... * @param date (optional) The date for which the astro time should be calculated. Default = today * @param offsetMinutes (optional) The number of minutes to be added to the return value. @@ -1528,11 +1357,12 @@ declare global { /** * Sets a state to the given value + * * @param id The ID of the state to be set */ function setState( id: string, - state:ioBroker.StateValue | ioBroker.SettableState, + state: ioBroker.StateValue | ioBroker.SettableState, callback?: iobJS.SetStateCallback, ): void; function setState( @@ -1550,6 +1380,7 @@ declare global { /** * Sets a state to the given value only if the value really changed. + * * @param id The ID of the state to be set */ function setStateChanged( @@ -1573,6 +1404,7 @@ declare global { /** * Sets a state to the given value after a timeout has passed. * Returns the timer, so it can be manually cleared with clearStateDelayed + * * @param id The ID of the state to be set * @param delay The delay in milliseconds * @param clearRunning (optional) Whether an existing timeout for this state should be cleared @@ -1621,6 +1453,7 @@ declare global { /** * Clears a timer created by setStateDelayed + * * @param id The state id for which the timer should be cleared * @param timerID (optional) ID of the specific timer to clear. If none is given, all timers are cleared. */ @@ -1628,11 +1461,13 @@ declare global { /** * Returns information about a specific timer created with `setStateDelayed`. + * * @param timerId The timer id that was returned by `setStateDelayed`. */ function getStateDelayed(timerId: number): iobJS.StateTimer | null; /** * Returns a list of all timers created with `setStateDelayed`. Can be limited to a specific state id. + * * @param id The state id for which the timers should be. */ function getStateDelayed(id?: string): iobJS.StateTimer[]; @@ -1661,6 +1496,7 @@ declare global { /** * Returns the IDs of the states with the given name + * * @param name Name of the state * @param forceArray (optional) Ensures that the return value is always an array, even if only one ID was found. */ @@ -1668,6 +1504,7 @@ declare global { /** * Reads an object from the object db. + * * @param enumName Which enum should be included in the returned object. `true` to return all enums. */ function getObject(id: T, enumName?: string | true): iobJS.ObjectIdToObjectType; @@ -1687,10 +1524,11 @@ declare global { function deleteObject(id: string, recursive: boolean, callback?: ErrorCallback): void; function deleteObjectAsync(id: string, recursive?: boolean): Promise; - function getEnums(enumName?: string): any; + function getEnums(enumName?: string): { id: string; members: string[]; name: ioBroker.StringOrTranslated }[]; /** - * Creates a state and the corresponding object under the javascript namespace. + * Creates a state and the corresponding object under the JavaScript namespace. + * * @param name The name of the state without the namespace * @param initValue (optional) Initial value of the state * @param forceCreation (optional) Override the state if it already exists @@ -1820,6 +1658,7 @@ declare global { /** * Deletes the state with the given ID + * * @param callback (optional) Is called after the state was deleted (or not). */ function deleteState(id: string, callback?: GenericCallback): void; @@ -1827,6 +1666,7 @@ declare global { /** * Sends a message to a specific instance or all instances of some specific adapter. + * * @param instanceName The instance to send this message to. * If the ID of an instance is given (e.g. "admin.0"), only this instance will receive the message. * If the name of an adapter is given (e.g. "admin"), all instances of this adapter will receive it. @@ -1869,6 +1709,7 @@ declare global { /** * Sends a message to a specific instance or all instances of some specific adapter. + * * @param host Host name. * @param command Command name for the target host. * @param message The message (e.g., params) to send. @@ -1886,7 +1727,8 @@ declare global { ): Promise; /** - * Creates a new notification (visible in admin adpter) + * Creates a new notification (visible in admin adapter) + * * @param msg Message text */ function registerNotification(msg: string): void; @@ -1902,6 +1744,7 @@ declare global { /** * Compares two or more times + * * @param timeToCompare - The time to compare with startTime and/or endTime. If none is given, the current time is used */ function compareTime( @@ -1930,6 +1773,7 @@ declare global { /** * Writes a file. + * * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" * @param name File name * @param data Contents of the file @@ -1940,6 +1784,7 @@ declare global { /** * Reads a file. + * * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" * @param name File name * @param callback Is called when the operation has finished (successfully or not) @@ -1949,6 +1794,7 @@ declare global { /** * Deletes a file. + * * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" * @param name File name * @param callback Is called when the operation has finished (successfully or not) @@ -1958,6 +1804,7 @@ declare global { /** * Deletes a file. + * * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" * @param name File name * @param callback Is called when the operation has finished (successfully or not) @@ -1967,6 +1814,7 @@ declare global { /** * Renames a file. + * * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" * @param oldName Current file name * @param newName New file name @@ -1977,6 +1825,7 @@ declare global { /** * Renames a file. + * * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" * @param oldName Current file name * @param newName New file name @@ -1990,6 +1839,7 @@ declare global { /** * Starts or restarts a script by name + * * @param scriptName (optional) Name of the script. If none is given, the current script is (re)started. */ function runScript(scriptName?: string, callback?: ErrorCallback): boolean; @@ -1997,6 +1847,7 @@ declare global { /** * Starts or restarts a script by name + * * @param scriptName (optional) Name of the script. If none is given, the current script is (re)started. * @param ignoreIfStarted If set to true, running scripts will not be restarted. * @param callback (optional) Is called when the script has finished (successfully or not) @@ -2006,16 +1857,18 @@ declare global { ignoreIfStarted: boolean, callback?: GenericCallback, ): boolean; - function startScriptAsync(scriptName?: string | undefined, ignoreIfStarted?: boolean): Promise; + function startScriptAsync(scriptName?: string, ignoreIfStarted?: boolean): Promise; /** * Starts or restarts a script by name + * * @param scriptName (optional) Name of the script. If none is given, the current script is (re)started. * @param callback (optional) Is called when the script has finished (successfully or not) */ function startScript(scriptName?: string, callback?: GenericCallback): boolean; /** * Stops a script by name + * * @param scriptName (optional) Name of the script. If none is given, the current script is stopped. */ function stopScript(scriptName: string | undefined, callback?: GenericCallback): boolean; @@ -2036,6 +1889,7 @@ declare global { /** * Digs in an object for the property value at the given path. + * * @param obj The object to dig in * @param path The path of the property to dig for in the given object */ @@ -2043,11 +1897,12 @@ declare global { /** * Sends a message to another script. + * * @param target Message name or target object * @param data Any data, that should be sent to message bus * @param options Actually only {timeout: X} is supported as option * @param callback Callback to get the result from other script - * @return ID of the subscription. It could be used for un-subscribe. + * @return ID of the subscription. It could be used for unsubscribe. */ function messageTo( target: iobJS.MessageTarget | string, @@ -2063,6 +1918,7 @@ declare global { /** * Process message from another script. + * * @param message Message name * @param callback Callback to send the result to another script */ @@ -2070,6 +1926,7 @@ declare global { /** * Unregister onmessage handler + * * @param id Message subscription id from onMessage or by message name * @return true if subscription exists and was deleted. */ @@ -2084,6 +1941,7 @@ declare global { /** * Receives logs of specified severity level in a script. + * * @param severity Severity level * @param callback Callback to send the result to another script */ @@ -2091,6 +1949,7 @@ declare global { /** * Unsubscribe log handler. + * * @param idOrCallbackOrSeverity Message subscription id from onLog or by callback function * @return true if subscription exists and was deleted. */ diff --git a/src/lib/mirror.ts b/src/lib/mirror.ts index c44563e7d..d7ca758c1 100644 --- a/src/lib/mirror.ts +++ b/src/lib/mirror.ts @@ -13,7 +13,7 @@ import { type FSWatcher, } from 'node:fs'; import { join, normalize, dirname } from 'node:path'; -import { ScriptType } from '../types'; +import type { ScriptType } from '../types'; const MODE_0777 = 511; @@ -72,8 +72,8 @@ export class Mirror { if (!existsSync(this.diskRoot)) { try { mkdirSync(this.diskRoot); - } catch (e) { - this.log.error(`Cannot create directory ${this.diskRoot}: ${e}`); + } catch (err: unknown) { + this.log.error(`Cannot create directory ${this.diskRoot}: ${err as Error}`); return; } } @@ -120,8 +120,8 @@ export class Mirror { }); } }); - } catch (err) { - return this.log.warn(`Error while trying to watch folder ${root_}: ${err}`); + } catch (err: unknown) { + return this.log.warn(`Error while trying to watch folder ${root_}: ${err as Error}`); } if (!this.watchedFolder[root_]) { @@ -140,7 +140,7 @@ export class Mirror { this.log.debug(`Folder ${root_} was deleted`); try { this.watchedFolder[root_] && this.watchedFolder[root_].close(); - } catch (e) { + } catch { // ignore error } @@ -243,7 +243,7 @@ export class Mirror { } private _getObjectsInPath(pathDB: string): string[] { - const reg = new RegExp(pathDB.replace(/\./g, '\\.') + '\\.[^.]+$'); + const reg = new RegExp(`${pathDB.replace(/\./g, '\\.')}\\.[^.]+$`); return Object.keys(this.dbList) .filter(id => reg.test(id)) .map(id => id.split('.').pop() || ''); @@ -263,8 +263,8 @@ export class Mirror { writeFileSync(diskListEntry.name, diskListEntry.source, { mode: MODE_0777 }); diskListEntry.ts = Date.now(); return true; - } catch (e) { - this.log.error(`Cannot write file ${diskListEntry ? diskListEntry.name : id}: ${e}`); + } catch (err: unknown) { + this.log.error(`Cannot write file ${diskListEntry ? diskListEntry.name : id}: ${err as Error}`); return false; } } @@ -417,8 +417,8 @@ export class Mirror { if (exists) { try { stats = statSync(file); - } catch (e) { - this.log.error(`Cannot read file ${file}: ${e}`); + } catch (err: unknown) { + this.log.error(`Cannot read file ${file}: ${err as Error}`); } if (stats?.isDirectory()) { @@ -431,8 +431,8 @@ export class Mirror { // update all files in this directory files.forEach(f => this.onFileChange('change', join(file, f).replace(/\\/g, '/'))); - } catch (err) { - this.log.error(`Error while checking files in directory ${file}: ${err}`); + } catch (err: unknown) { + this.log.error(`Error while checking files in directory ${file}: ${err as Error}`); } } else { if (this.watchedFolder[file]) { @@ -450,7 +450,7 @@ export class Mirror { this.watchedFolder[file].close(); delete this.watchedFolder[file]; } - } catch (e) { + } catch { // ignore error } this.removeScriptsInFolder(file); @@ -466,7 +466,7 @@ export class Mirror { this.watchedFolder[file].close(); delete this.watchedFolder[file]; } - } catch (e) { + } catch { // ignore error } this.removeScriptsInFolder(file); @@ -518,14 +518,14 @@ export class Mirror { this._checkIfAllFoldersAreExist(id, this.dbList); this.adapter.setForeignObject(id, this.dbList[id] as ioBroker.Object); } - } catch (e) { - this.log.error(`Cannot read file ${file}: ${e}`); + } catch (err: unknown) { + this.log.error(`Cannot read file ${file}: ${err as Error}`); } } else if (event === 'delete' || event === 'rename') { // there is a bug: just after creation of a script on disk from ioBroker, "rename" event is generated! if (event === 'rename' && this.diskList[id] && Date.now() - this.diskList[id].ts < 200) { console.log(`Interval: ${Date.now() - this.diskList[id].ts}`); - // 'rename is ignored + // 'rename' is ignored this.log.debug(`Rename event for ${id} is ignored`); } else { if (this.dbList[id]) { @@ -560,8 +560,8 @@ export class Mirror { try { this.log.debug(`Delete ${file} on disk`); unlinkSync(file); - } catch (e) { - this.log.error(`Cannot delete ${file}: ${e}`); + } catch (err: unknown) { + this.log.error(`Cannot delete ${file}: ${err as Error}`); } } if (this.diskList[id]) { @@ -619,8 +619,8 @@ export class Mirror { } }); } - } catch (err) { - this.log.error(`Error while checking files in directory ${dirPath}: ${err}`); + } catch (err: unknown) { + this.log.error(`Error while checking files in directory ${dirPath}: ${err as Error}`); } return list; } @@ -641,15 +641,13 @@ export class Mirror { }; list[folderId] = obj; try { - this.adapter.setForeignObject( - folderId, - obj, - err => - err && - this.log.warn(`Error while checking script folder ${folderId} for id "${id}": ${err}`), - ); - } catch (err) { - this.log.warn(`Error while checking script folder ${folderId} for id "${id}": ${err}`); + this.adapter.setForeignObject(folderId, obj, err => { + if (err) { + this.log.warn(`Error while checking script folder ${folderId} for id "${id}": ${err}`); + } + }); + } catch (err: unknown) { + this.log.warn(`Error while checking script folder ${folderId} for id "${id}": ${err as Error}`); } } } @@ -665,7 +663,7 @@ export class Mirror { // adapter.subscribeForeignObjects('script.js.*'); const list = {}; if (err) { - this.log.error('Error while checking scripts channel from objects database: ' + err); + this.log.error(`Error while checking scripts channel from objects database: ${err}`); } if (res && res.rows) { for (let i = 0; i < res.rows.length; i++) { diff --git a/src/lib/patternCompareFunctions.ts b/src/lib/patternCompareFunctions.ts index 44a4aab51..e7a794ede 100644 --- a/src/lib/patternCompareFunctions.ts +++ b/src/lib/patternCompareFunctions.ts @@ -1,4 +1,4 @@ -import { Pattern } from '../types'; +import type { Pattern } from '../types'; import { type EventObj } from './eventObj'; function isRegExp(obj: any): boolean { diff --git a/src/lib/protectFs.ts b/src/lib/protectFs.ts index a8635e3c7..7a195440a 100644 --- a/src/lib/protectFs.ts +++ b/src/lib/protectFs.ts @@ -1,6 +1,7 @@ -import { +import type { BufferEncodingOption, CopyOptions, + Dirent, MakeDirectoryOptions, Mode, ObjectEncodingOptions, @@ -11,20 +12,86 @@ import { StatOptions, Stats, TimeLike, + CopySyncOptions, + NoParamCallback, + PathOrFileDescriptor, + WriteFileOptions, } from 'node:fs'; -import { Abortable } from 'node:events'; -import { FileHandle, FlagAndOpenMode } from 'fs/promises'; -import { CopySyncOptions, NoParamCallback, PathOrFileDescriptor } from 'fs'; -import { URL } from 'node:url'; -import { Stream } from 'node:stream'; +import type { Abortable } from 'node:events'; +import type { FileHandle, FlagAndOpenMode } from 'node:fs/promises'; +import type { URL } from 'node:url'; +import type { Stream } from 'node:stream'; -const nodeFS = require('node:fs'); -const { sep, normalize, join } = require('node:path'); +import * as nodeFS from 'node:fs'; +import { sep, normalize, join } from 'node:path'; export default class ProtectFs { private readonly log: ioBroker.Logger; private readonly ioBrokerDataDir: string; - public readonly promises: Record; + public readonly promises: { + access: (path: PathLike, mode?: number) => Promise; + cp: (source: string | URL, destination: string | URL, opts?: CopyOptions) => Promise; + readFile: ( + path: PathLike | FileHandle, + options: + | ({ + encoding: BufferEncoding; + flag?: OpenMode | undefined; + } & Abortable) + | BufferEncoding, + ) => Promise; + readlink: (path: PathLike, options: BufferEncodingOption) => Promise; + symlink: (target: PathLike, path: PathLike, type?: string | null) => Promise; + writeFile: ( + file: PathLike | FileHandle, + data: + | string + | NodeJS.ArrayBufferView + | Iterable + | AsyncIterable + | Stream, + options?: + | (ObjectEncodingOptions & { + mode?: Mode | undefined; + flag?: OpenMode | undefined; + flush?: boolean | undefined; + } & Abortable) + | BufferEncoding + | null, + ) => Promise; + unlink: (path: PathLike) => Promise; + appendFile: ( + path: PathLike | FileHandle, + data: string | Uint8Array, + options?: + | (ObjectEncodingOptions & FlagAndOpenMode & { flush?: boolean | undefined }) + | BufferEncoding + | null, + ) => Promise; + chmod: (path: PathLike, mode: Mode) => Promise; + copyFile: (src: PathLike, dest: PathLike, mode?: number) => Promise; + rename: (oldPath: PathLike, newPath: PathLike) => Promise; + open: (path: PathLike, flags?: string | number, mode?: Mode) => Promise; + truncate: (path: PathLike, len?: number) => Promise; + stat: (path: PathLike, opts?: StatOptions) => Promise; + utimes: (path: PathLike, atime: TimeLike, mtime: TimeLike) => Promise; + readdir: ( + path: PathLike, + options?: ObjectEncodingOptions & { + withFileTypes: true; + recursive?: boolean | undefined; + }, + ) => Promise; + lchmod: (path: PathLike, mode: Mode) => Promise; + lchown: (path: PathLike, uid: number, gid: number) => Promise; + link: (existingPath: PathLike, newPath: PathLike) => Promise; + lstat: (path: PathLike, opts?: StatOptions) => Promise; + lutimes: (path: PathLike, atime: TimeLike, mtime: TimeLike) => Promise; + mkdir: (path: PathLike, options?: Mode | MakeDirectoryOptions | null) => Promise; + mkdtemp: (prefix: string, options?: ObjectEncodingOptions | BufferEncoding | null) => Promise; + rm: (path: PathLike, options?: RmOptions) => Promise; + rmdir: (path: PathLike, options?: RmDirOptions) => Promise; + }; public readonly constants: Record; constructor(log: ioBroker.Logger, ioBrokerDataDir: string) { @@ -67,7 +134,7 @@ export default class ProtectFs { symlink: async (target: PathLike, path: PathLike, type?: string | null): Promise => { this.#checkProtected(target, true); this.#checkProtected(path, false); - return nodeFS.promises.symlink(target, path); // async function symlink(target, path, type_) { + return nodeFS.promises.symlink(target, path, type); // async function symlink(target, path, type_) { }, writeFile: async ( file: PathLike | FileHandle, @@ -84,6 +151,7 @@ export default class ProtectFs { /** * If all data is successfully written to the file, and `flush` * is `true`, `filehandle.sync()` is used to flush the data. + * * @default false */ flush?: boolean | undefined; @@ -133,7 +201,8 @@ export default class ProtectFs { }, stat: async (path: PathLike, opts?: StatOptions): Promise => { this.#checkProtected(path, true); - return nodeFS.promises.stat.call(this, path, opts); // async function stat(path, options = { bigint: false }) { + const result = await nodeFS.promises.stat.call(this, path, opts); // async function stat(path, options = { bigint: false }) { + return result as Stats; }, utimes: async (path: PathLike, atime: TimeLike, mtime: TimeLike): Promise => { this.#checkProtected(path, false); @@ -145,9 +214,9 @@ export default class ProtectFs { withFileTypes: true; recursive?: boolean | undefined; }, - ) => { + ): Promise => { this.#checkProtected(path, true); - return nodeFS.promises.readdir.call(this, path, options); // async function readdir(path, options) { + return nodeFS.promises.readdir.call(this, path, options || { encoding: null, withFileTypes: true }); // async function readdir(path, options) { }, lchmod: async (path: PathLike, mode: Mode): Promise => { this.#checkProtected(path, false); @@ -164,7 +233,8 @@ export default class ProtectFs { }, lstat: async (path: PathLike, opts?: StatOptions): Promise => { this.#checkProtected(path, true); - return nodeFS.promises.lstat.call(this, path, opts); // async function lstat(path, options = { bigint: false }) { + const res = await nodeFS.promises.lstat.call(this, path, opts); // async function lstat(path, options = { bigint: false }) { + return res as Stats; }, lutimes: async (path: PathLike, atime: TimeLike, mtime: TimeLike): Promise => { this.#checkProtected(path, false); @@ -177,9 +247,13 @@ export default class ProtectFs { this.#checkProtected(path, false); return nodeFS.promises.mkdir.call(this, path, options); // async function mkdir(path, options) { }, - mkdtemp: async (prefix: string, options: BufferEncodingOption): Promise => { + mkdtemp: async ( + prefix: string, + options?: ObjectEncodingOptions | BufferEncoding | null, + ): Promise => { this.#checkProtected(prefix, false); - return nodeFS.promises.mkdtemp.call(this, prefix, options); // async function mkdtemp(prefix, options) { + const tmp = await nodeFS.promises.mkdtemp.call(this, prefix, options); // async function mkdtemp(prefix, options) { + return tmp.toString(); }, rm: async (path: PathLike, options?: RmOptions): Promise => { this.#checkProtected(path, false); @@ -214,16 +288,21 @@ export default class ProtectFs { } } - #checkProtected(file: PathLike | FileHandle, readonly: boolean): void { - const filePath = normalize(file.toString()); + #checkProtected(file: PathLike | FileHandle, readOnly: boolean): void { + if ((file as FileHandle).fd) { + return; + } + const filePath = normalize((file as PathLike).toString()); // todo: protect against file://... if (filePath.endsWith(`-data${sep}objects.json`) || filePath.endsWith(`-data${sep}objects.jsonl`)) { - this.log.error(`May not read ${file}`); + this.log.error(`May not read ${(file as PathLike).toString()}`); throw new Error('Permission denied'); } - if (!readonly && filePath.startsWith(join(this.ioBrokerDataDir, 'files'))) { - this.log.error(`May not read ${file} - use ${readonly ? 'readFile' : 'writeFile'} instead`); + if (!readOnly && filePath.startsWith(join(this.ioBrokerDataDir, 'files'))) { + this.log.error( + `May not read ${(file as PathLike).toString()} - use ${readOnly ? 'readFile' : 'writeFile'} instead`, + ); throw new Error('Permission denied'); } } @@ -243,13 +322,16 @@ export default class ProtectFs { destination: string | URL, opts: CopyOptions | ((err: NodeJS.ErrnoException | null) => void), callback?: (err: NodeJS.ErrnoException | null) => void, - ) { + ): void { this.#checkProtected(source, false); this.#checkProtected(destination, false); if (callback) { return nodeFS.cp(source, destination, opts as CopyOptions, callback); } - return nodeFS.cp(source, destination, opts); + if (typeof opts === 'function') { + return nodeFS.cp(source, destination, opts); + } + return nodeFS.cp(source, destination, opts as (err: NodeJS.ErrnoException | null) => void); } cpSync(source: string | URL, destination: string | URL, opts?: CopySyncOptions): void { @@ -280,103 +362,131 @@ export default class ProtectFs { return nodeFS.readFileSync.call(this, path, options); // function readFileSync(path, options) { } - readlink() { - this.#checkProtected(arguments[0], true); - return nodeFS.readlink.apply(this, arguments); // function readlink(path, options, callback) { + readlink(path: PathLike, callback: (err: NodeJS.ErrnoException | null, linkString: string) => void): void { + this.#checkProtected(path, true); + return nodeFS.readlink.call(this, path, callback); // function readlink(path, options, callback) { } - readlinkSync() { - this.#checkProtected(arguments[0], true); - return nodeFS.readlinkSync.apply(this, arguments); // function readlinkSync(path, options) { + readlinkSync(path: PathLike, options?: any): string | Buffer { + this.#checkProtected(path, true); + return nodeFS.readlinkSync.call(this, path, options); // function readlinkSync(path, options) { } - symlink() { - this.#checkProtected(arguments[0], true); - this.#checkProtected(arguments[0], false); - return nodeFS.symlink.apply(this, arguments); // function symlink(target, path, type_, callback_) { + symlink( + target: PathLike, + path: PathLike, + type: 'dir' | 'file' | 'junction' | undefined | null | NoParamCallback, + callback?: NoParamCallback, + ): void { + this.#checkProtected(target, true); + this.#checkProtected(path, false); + if (typeof callback === 'function') { + // @ts-expect-error should work + return nodeFS.symlink.call(this, target, path, type, callback); + } + return nodeFS.symlink.call(this, target, path, type as NoParamCallback); // function symlink(target, path, type_, callback_) { } - symlinkSync() { - this.#checkProtected(arguments[0], true); - this.#checkProtected(arguments[0], false); - return nodeFS.symlinkSync.apply(this, arguments); // function symlinkSync(target, path, type) { + symlinkSync(target: PathLike, path: PathLike, type?: 'dir' | 'file' | 'junction' | null): void { + this.#checkProtected(target, true); + this.#checkProtected(path, false); + return nodeFS.symlinkSync.call(this, target, path, type); // function symlinkSync(target, path, type) { } - writeFile() { - this.#checkProtected(arguments[0], false); - return nodeFS.writeFile.apply(this, arguments); // function writeFile(path, data, options, callback) { + writeFile( + file: PathLike | number, + data: string | NodeJS.ArrayBufferView, + options?: WriteFileOptions, + callback?: NoParamCallback, + ): void { + if (typeof file !== 'number') { + this.#checkProtected(file, false); + } + // @ts-expect-error should work + return nodeFS.writeFile.call(this, file, data, options, callback); // function writeFile(path, data, options, callback) { } - writeFileSync() { - this.#checkProtected(arguments[0], false); - return nodeFS.writeFileSync.apply(this, arguments); // function writeFileSync(path, data, options) { + writeFileSync(file: PathLike | number, data: string | NodeJS.ArrayBufferView, options?: WriteFileOptions): void { + if (typeof file !== 'number') { + this.#checkProtected(file, false); + } + return nodeFS.writeFileSync.call(this, file, data, options); // function writeFileSync(path, data, options) { } - unlink() { - this.#checkProtected(arguments[0], false); - return nodeFS.unlink.apply(this, arguments); // function unlink(path, callback) { + unlink(path: PathLike, callback?: NoParamCallback): void { + this.#checkProtected(path, false); + // @ts-expect-error should work + return nodeFS.unlink.call(this, path, callback); // function unlink(path, callback) { } - unlinkSync() { - this.#checkProtected(arguments[0], false); - return nodeFS.unlinkSync.apply(this, arguments); // function unlinkSync(path) { + unlinkSync(path: PathLike): void { + this.#checkProtected(path, false); + return nodeFS.unlinkSync.call(this, path); // function unlinkSync(path) { } - appendFile() { - this.#checkProtected(arguments[0], false); - return nodeFS.appendFile.apply(this, arguments); // function appendFile(path, data, options, callback) { + appendFile(file: PathLike | number, data: string | Uint8Array, callback?: NoParamCallback): void { + if (typeof file !== 'number') { + this.#checkProtected(file, false); + } + // @ts-expect-error should work + return nodeFS.appendFile.call(this, file, data, callback); // function appendFile(path, data, options, callback) { } - appendFileSync() { - this.#checkProtected(arguments[0], false); - return nodeFS.appendFileSync.apply(this, arguments); // function appendFileSync(path, data, options) { + appendFileSync(file: PathLike | number, data: string | Uint8Array): void { + if (typeof file !== 'number') { + this.#checkProtected(file, false); + } + return nodeFS.appendFileSync.call(this, file, data); // function appendFileSync(path, data, options) { } - chmod() { - this.#checkProtected(arguments[0], false); - return nodeFS.chmod.apply(this, arguments); // function chmod(path, mode, callback) { + chmod(path: PathLike, mode: Mode, callback?: NoParamCallback): void { + this.#checkProtected(path, false); + // @ts-expect-error should work + return nodeFS.chmod.call(this, path, mode, callback); // function chmod(path, mode, callback) { } - chmodSync() { - this.#checkProtected(arguments[0], false); - return nodeFS.chmodSync.apply(this, arguments); // function chmodSync(path, mode) { + chmodSync(path: PathLike, mode: Mode): void { + this.#checkProtected(path, false); + return nodeFS.chmodSync.call(this, path, mode); // function chmodSync(path, mode) { } - chown() { - this.#checkProtected(arguments[0], false); - return nodeFS.chmodSync.apply(this, arguments); // function chown(path, uid, gid, callback) { + chown(path: PathLike, uid: number, gid: number, callback?: NoParamCallback): void { + this.#checkProtected(path, false); + // @ts-expect-error should work + return nodeFS.chown.call(this, path, uid, gid, callback); // function chown(path, uid, gid, callback) { } - chownSync() { - this.#checkProtected(arguments[0], false); - return nodeFS.chownSync.apply(this, arguments); // function chownSync(path, uid, gid) { + chownSync(path: PathLike, uid: number, gid: number): void { + this.#checkProtected(path, false); + return nodeFS.chownSync.call(this, path, uid, gid); // function chownSync(path, uid, gid) { } - copyFile() { - this.#checkProtected(arguments[0], false); - this.#checkProtected(arguments[1], false); - return nodeFS.copyFile.apply(this, arguments); // function copyFile(src, dest, mode, callback) { + copyFile(src: PathLike, dest: PathLike, mode?: number | NoParamCallback, callback?: NoParamCallback): void { + this.#checkProtected(src, true); + this.#checkProtected(dest, false); + // @ts-expect-error should work + return nodeFS.copyFile.call(this, src, dest, mode, callback); // function copyFile(src, dest, mode, callback) { } - copyFileSync() { - this.#checkProtected(arguments[0], true); - this.#checkProtected(arguments[1], false); - return nodeFS.copyFileSync.apply(this, arguments); // function copyFileSync(src, dest, mode) { + copyFileSync(src: PathLike, dest: PathLike, mode?: number): void { + this.#checkProtected(src, true); + this.#checkProtected(dest, false); + return nodeFS.copyFileSync.call(this, src, dest, mode); // function copyFileSync(src, dest, mode) { } - rename() { + rename(): void { this.#checkProtected(arguments[0], false); this.#checkProtected(arguments[1], false); return nodeFS.rename.apply(this, arguments); // function rename(oldPath, newPath, callback) { } - renameSync() { + renameSync(): void { this.#checkProtected(arguments[0], false); this.#checkProtected(arguments[1], false); return nodeFS.renameSync.apply(this, arguments); // function renameSync(oldPath, newPath) { } - open() { + open(): void { this.#checkProtected(arguments[0], true); return nodeFS.open.apply(this, arguments); // function open(path, flags, mode, callback) { } @@ -386,99 +496,99 @@ export default class ProtectFs { return nodeFS.openSync.apply(this, arguments); // function openSync(path, flags, mode) { } - truncate() { + truncate(): void { this.#checkProtected(arguments[0], false); return nodeFS.truncate.apply(this, arguments); // function truncate(path, len, callback) { } - truncateSync() { + truncateSync(): void { this.#checkProtected(arguments[0], false); return nodeFS.truncateSync.apply(this, arguments); // function truncateSync(path, len) { } - exists() { + exists(): void { this.#checkProtected(arguments[0], true); return nodeFS.exists.apply(this, arguments); // function exists(path, callback) { } - existsSync() { + existsSync(): boolean { this.#checkProtected(arguments[0], true); return nodeFS.existsSync.apply(this, arguments); // function existsSync(path) { } - stat() { + stat(): void { this.#checkProtected(arguments[0], true); return nodeFS.stat.apply(this, arguments); // function stat(path, options = { bigint: false }, callback) { } - statSync() { + statSync(): void { this.#checkProtected(arguments[0], true); return nodeFS.statSync.apply(this, arguments); // function statSync(path, options = { bigint: false, throwIfNoEntry: true }) { } - utimes() { + utimes(): void { this.#checkProtected(arguments[0], false); return nodeFS.utimes.apply(this, arguments); // function utimes(path, atime, mtime, callback) { } - utimesSync() { + utimesSync(): void { this.#checkProtected(arguments[0], false); return nodeFS.utimesSync.apply(this, arguments); // function utimesSync(path, atime, mtime) { } - readdir() { + readdir(): void { this.#checkProtected(arguments[0], true); return nodeFS.readdir.apply(this, arguments); // function readdir(path, options, callback) { } - readdirSync() { + readdirSync(): void { this.#checkProtected(arguments[0], true); return nodeFS.readdirSync.apply(this, arguments); // function readdirSync(path, options) { } - createReadStream() { + createReadStream(): void { this.#checkProtected(arguments[0], true); return nodeFS.createReadStream.apply(this, arguments); // function createReadStream(path, options) { } - createWriteStream() { + createWriteStream(): void { this.#checkProtected(arguments[0], false); return nodeFS.createWriteStream.apply(this, arguments); // function createWriteStream(path, options) { } - lchmod() { + lchmod(): void { this.#checkProtected(arguments[0], false); return nodeFS.lchmod.apply(this, arguments); // function lchmod(path, mode, callback) { } - lchmodSync() { + lchmodSync(): void { this.#checkProtected(arguments[0], false); return nodeFS.lchmodSync.apply(this, arguments); // function lchmodSync(path, mode) { } - lchown() { + lchown(): void { this.#checkProtected(arguments[0], false); return nodeFS.lchown.apply(this, arguments); // function lchown(path, uid, gid, callback) { } - lchownSync() { + lchownSync(): void { this.#checkProtected(arguments[0], false); return nodeFS.lchownSync.apply(this, arguments); // function lchownSync(path, uid, gid) { } - link() { + link(): void { this.#checkProtected(arguments[0], false); this.#checkProtected(arguments[1], false); return nodeFS.link.apply(this, arguments); // function link(existingPath, newPath, callback) { } - linkSync() { + linkSync(): void { this.#checkProtected(arguments[0], false); this.#checkProtected(arguments[1], false); return nodeFS.linkSync.apply(this, arguments); // function linkSync(existingPath, newPath) { } - lstat() { + lstat(): void { this.#checkProtected(arguments[0], true); return nodeFS.lstat.apply(this, arguments); // function lstat(path, options = { bigint: false }, callback) { } @@ -488,7 +598,7 @@ export default class ProtectFs { return nodeFS.lstatSync.apply(this, arguments); // function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) { } - lutimes() { + lutimes(): void { this.#checkProtected(arguments[0], false); return nodeFS.lutimes.apply(this, arguments); // function lutimes(path, atime, mtime, callback) { } diff --git a/src/lib/request.ts b/src/lib/request.ts index 348d2eb57..144a5ffbd 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -13,24 +13,20 @@ let logger: { function requestError(error: Error | string): void { logger.error(`Request error: ${error}`); } -type METHODS = 'get' | 'post' | 'put' | 'head' | 'patch' | 'del' | 'delete' | 'initParams'; /** * Calls a request method which accepts a callback and handles errors in the request and the callback itself */ -function requestSafe( - method: (..._args: any[]) => any, - ...args: any[] -) { +function requestSafe(method: (..._args: any[]) => any, ...args: any[]): any { const lastArg = args[args.length - 1]; if (typeof lastArg === 'function') { // If a callback was provided, handle errors in the callback const otherArgs = args.slice(0, args.length - 1); - return method(...otherArgs, (...cbArgs) => { + return method(...otherArgs, (...cbArgs: any[]): void => { try { lastArg(...cbArgs); - } catch (e) { - logger.error(`Error in request callback: ${e}`); + } catch (err: unknown) { + logger.error(`Error in request callback: ${err as Error}`); } }).on('error', requestError); } @@ -42,6 +38,7 @@ function requestSafe( // Wrap all methods that accept a callback const methodsWithCallback = ['get', 'post', 'put', 'head', 'patch', 'del', 'delete', 'initParams']; // and request itself +// @ts-expect-error fix later const request = (...args: any[]): any => requestSafe(_request, ...args); for (const methodName of methodsWithCallback) { @@ -54,9 +51,7 @@ for (const propName of otherPropsAndMethods) { request[propName] = _request[propName]; } -request.setLogger = function (_logger: { - error: (e: string) => void; -}): void { +request.setLogger = function (_logger: { error: (e: string) => void }): void { logger = _logger; }; diff --git a/src/lib/sandbox.ts b/src/lib/sandbox.ts index 7a79a12a9..8c5f30953 100644 --- a/src/lib/sandbox.ts +++ b/src/lib/sandbox.ts @@ -1,13 +1,13 @@ -import { type ChildProcess, ExecOptions } from 'node:child_process'; +import type { ChildProcess, ExecOptions } from 'node:child_process'; import * as jsonataMod from 'jsonata'; -import { type SendMailOptions } from 'nodemailer'; -import { AxiosHeaderValue, AxiosResponse, ResponseType } from 'axios'; +import type { SendMailOptions } from 'nodemailer'; +import type { AxiosError, AxiosHeaderValue, AxiosResponse, ResponseType } from 'axios'; import { commonTools } from '@iobroker/adapter-core'; import { isObject, isArray, promisify, getHttpRequestConfig } from './tools'; -import { - AdapterConfig, +import type { + JavaScriptAdapterConfig, AstroRule, ChangeType, CommonAlias, @@ -28,10 +28,11 @@ import * as constsMod from './consts'; import * as wordsMod from './words'; import * as eventObjMod from './eventObj'; import { patternCompareFunctions as patternCompareFunctionsMod } from './patternCompareFunctions'; -import { type PatternEventCompareFunction } from './patternCompareFunctions'; -import { ScheduleName, SchedulerRule } from './scheduler'; -import { EventObj } from './eventObj'; -import { AstroEvent } from './consts'; +import type { PatternEventCompareFunction } from './patternCompareFunctions'; +import type { ScheduleName, SchedulerRule } from './scheduler'; +import type { EventObj } from './eventObj'; +import type { AstroEvent } from './consts'; + const pattern2RegEx = commonTools.pattern2RegEx; export function sandBox( @@ -45,7 +46,7 @@ export function sandBox( const words = wordsMod; const eventObj = eventObjMod; const patternCompareFunctions = patternCompareFunctionsMod; - const jsonata = jsonataMod; + const jsonata = jsonataMod.default; const adapter: ioBroker.Adapter = context.adapter; const mods = context.mods; @@ -54,9 +55,11 @@ export function sandBox( const timers = context.timers; const enums = context.enums; const debugMode = context.debugMode; + + // eslint-disable-next-line prefer-const let sandbox: SandboxType; - function errorInCallback(e) { + function errorInCallback(e: Error): void { adapter.setState(`scriptProblem.${name.substring('script.js.'.length)}`, { val: true, ack: true, @@ -67,7 +70,7 @@ export function sandBox( } function subscribePattern(script: JsScript, pattern: string): void { - if ((adapter.config as AdapterConfig).subscribe) { + if ((adapter.config as JavaScriptAdapterConfig).subscribe) { if (!script.subscribes[pattern]) { script.subscribes[pattern] = 1; } else { @@ -77,12 +80,14 @@ export function sandBox( if (!context.subscribedPatterns[pattern]) { context.subscribedPatterns[pattern] = 1; - sandbox.verbose && sandbox.log(`subscribePattern(pattern=${pattern})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`subscribePattern(pattern=${pattern})`, 'info'); + } adapter.subscribeForeignStates(pattern); // request current value to deliver old value on change. if (typeof pattern === 'string' && !pattern.includes('*')) { - adapter.getForeignState(pattern, (err, state) => { + adapter.getForeignState(pattern, (_err, state) => { if (state) { states[pattern] = state; } @@ -90,7 +95,7 @@ export function sandBox( } else { adapter.getForeignStates( pattern, - (err, _states) => _states && Object.keys(_states).forEach(id => (states[id] = _states[id])), + (_err, _states) => _states && Object.keys(_states).forEach(id => (states[id] = _states[id])), ); } } else { @@ -100,7 +105,7 @@ export function sandBox( } function unsubscribePattern(script: JsScript, pattern: string): void { - if ((adapter.config as AdapterConfig).subscribe) { + if ((adapter.config as JavaScriptAdapterConfig).subscribe) { if (script.subscribes[pattern]) { script.subscribes[pattern]--; if (!script.subscribes[pattern]) { @@ -267,7 +272,6 @@ export function sandBox( /** * Returns the `common.type` for a given variable - * @param {any} value */ function getCommonTypeOf(value: any): ioBroker.CommonType { return isArray(value) ? 'array' : isObject(value) ? 'object' : (typeof value as ioBroker.CommonType); @@ -307,13 +311,13 @@ export function sandBox( continue; } context.folderCreationVerifiedObjects[idToCheck] = true; - let obj; + let obj: ioBroker.Object | null | undefined; try { obj = await adapter.getForeignObjectAsync(idToCheck); - } catch (err) { + } catch { // ignore } - if (!obj || !obj.common) { + if (!obj?.common) { sandbox.log(`Create folder object for ${idToCheck}`, 'debug'); try { await adapter.setForeignObjectAsync(idToCheck, { @@ -341,7 +345,7 @@ export function sandBox( isChanged: boolean, id: string, state: null | ioBroker.SettableState | ioBroker.StateValue, - isAck: boolean | 'true' | 'false' | undefined | (() => void), + isAck: boolean | 'true' | 'false' | undefined | ((error?: Error | null) => void), callback?: (error?: Error | null) => void, ): void { if (typeof isAck === 'function') { @@ -403,7 +407,7 @@ export function sandBox( } // If this is not the expected one, issue a warning if (actualCommonType && actualCommonType !== common.type) { - context.logWithLineInfo?.warn( + context.logWithLineInfo( `You are assigning a ${actualCommonType} to the state "${id}" which expects a ${common.type}. ` + `Please fix your code to use a ${common.type} or change the state type to ${actualCommonType}. ` + `This warning might become an error in future versions.`, @@ -418,17 +422,19 @@ export function sandBox( stateNotNull = JSON.stringify(stateNotNull); } } catch (err: any) { - context.logWithLineInfo?.warn( + context.logWithLineInfo( `Could not stringify value for type ${actualCommonType} and id ${id}: ${err.message}`, ); if (typeof callback === 'function') { try { callback.call( sandbox, - `Could not stringify value for type ${actualCommonType} and id ${id}: ${err.message}`, + new Error( + `Could not stringify value for type ${actualCommonType} and id ${id}: ${err.message}`, + ), ); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } } @@ -437,7 +443,7 @@ export function sandBox( // Check min and max of value if (typeof stateNotNull === 'object') { if (common && typeof stateNotNull.val === 'number') { - const num: number = stateNotNull.val as number; + const num: number = stateNotNull.val; if (common.min !== undefined && num < common.min) { stateNotNull.val = common.min; } else if (common.max !== undefined && num > common.max) { @@ -456,7 +462,11 @@ export function sandBox( let stateAsObject: ioBroker.State; // modify state here, to make it available in callback - if (typeof stateNotNull !== 'object' || (stateNotNull as ioBroker.SettableState).val === undefined) { + if ( + stateNotNull === null || + typeof stateNotNull !== 'object' || + (stateNotNull as ioBroker.SettableState).val === undefined + ) { stateAsObject = context.prepareStateObject(id, { val: stateNotNull as ioBroker.StateValue, ack: isAck === true || isAck === 'true', @@ -470,7 +480,9 @@ export function sandBox( if (objects[id]) { script.setStatePerMinuteCounter++; - sandbox.verbose && sandbox.log(`setForeignState(id=${id}, state=${JSON.stringify(stateAsObject)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`setForeignState(id=${id}, state=${JSON.stringify(stateAsObject)})`, 'info'); + } if (debug) { sandbox.log( @@ -482,13 +494,13 @@ export function sandBox( setImmediate(() => { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }); } } else { - if (!(adapter.config as AdapterConfig).subscribe) { + if (!(adapter.config as JavaScriptAdapterConfig).subscribe) { // store actual state to make possible to process value in callback // risk that there will be an error on setState is very low, // but we will not store new state if the setStateChanged is called @@ -499,7 +511,7 @@ export function sandBox( const errHandler = (err: Error | null | undefined, funcId: string): void => { err && sandbox.log(`${funcId}: ${err}`, 'error'); // If adapter holds all states - if (err && !(adapter.config as AdapterConfig).subscribe) { + if (err && !(adapter.config as JavaScriptAdapterConfig).subscribe) { delete context.interimStateValues[id]; } @@ -507,14 +519,14 @@ export function sandBox( setImmediate(() => { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }); } }; if (isChanged) { - if (!(adapter.config as AdapterConfig).subscribe && context.interimStateValues[id]) { + if (!(adapter.config as JavaScriptAdapterConfig).subscribe && context.interimStateValues[id]) { // if the state is changed, we will compare it with interimStateValues const oldState = context.interimStateValues[id], attrs = Object.keys(stateAsObject).filter( @@ -540,13 +552,13 @@ export function sandBox( } } } else { - context.logWithLineInfo?.warn(`State "${id}" not found`); + context.logWithLineInfo(`State "${id}" not found`); if (typeof callback === 'function') { setImmediate(() => { try { - callback.call(sandbox, `State "${id}" not found`); - } catch (e) { - errorInCallback(e); + callback.call(sandbox, new Error(`State "${id}" not found`)); + } catch (err: unknown) { + errorInCallback(err as Error); } }); } @@ -595,7 +607,7 @@ export function sandBox( try { // the user requires a module which is not specified in the additional node modules - // for backward compatibility we check if the module can simply be required directly before we fail (e.g. direct dependencies of javascript adapter) + // for backward compatibility we check if the module can simply be required directly before we fail (e.g., direct dependencies of JavaScript adapter) adapter.log.debug(`Try direct require of "${md}" as not specified in the additional dependencies`); mods[md] = require(md); @@ -637,7 +649,7 @@ export function sandBox( const result: iobJS.QueryResult = {} as iobJS.QueryResult; - let name: string = ''; + let name = ''; const commonStrings: string[] = []; const enumStrings: string[] = []; const nativeStrings: string[] = []; @@ -741,7 +753,6 @@ export function sandBox( return result; } - /** @type {Selector[]} */ let commonSelectors: Selector[] = commonStrings.map(selector => splitSelectorString(selector)); let nativeSelectors: Selector[] = nativeStrings.map(selector => splitSelectorString(selector)); const enumSelectorObjects: Selector[] = enumStrings.map(_enum => splitSelectorString(_enum)); @@ -893,9 +904,10 @@ export function sandBox( if (!context.channels) { // TODO: fill the channels and maintain them on all places where context.stateIds will be changed } + const channels = context.channels || {}; // go through all channels - res = Object.keys(context.channels); + res = Object.keys(channels); // filter out those that don't match every ID selector for the channel ID if (objectIdSelectors.length) { res = res.filter(channelId => applyIDSelectors(channelId, objectIdSelectors)); @@ -915,7 +927,7 @@ export function sandBox( // retrieve the state ID collection for all remaining channels res = res - .map(id => context.channels[id]) + .map(id => channels[id]) // and flatten the array to get only the state IDs .reduce((acc, next) => acc.concat(next), []); @@ -928,8 +940,10 @@ export function sandBox( // TODO: fill the devices and maintain them on all places where context.stateIds will be changed } + const devices = context.devices || {}; + // go through all devices - res = Object.keys(context.devices); + res = Object.keys(devices); // filter out those that don't match every ID selector for the channel ID if (objectIdSelectors.length) { res = res.filter(deviceId => applyIDSelectors(deviceId, objectIdSelectors)); @@ -952,7 +966,7 @@ export function sandBox( // retrieve the state ID collection for all remaining devices res = res - .map(id => context.devices[id]) + .map(id => devices[id]) // and flatten the array to get only the state IDs .reduce((acc, next) => acc.concat(next), []); @@ -995,7 +1009,7 @@ export function sandBox( } } - const resUnique = []; + const resUnique: string[] = []; for (let i = 0; i < res.length; i++) { if (!resUnique.includes(res[i])) { resUnique.push(res[i]); @@ -1032,17 +1046,20 @@ export function sandBox( result.getState = function ( callback?: iobJS.GetStateCallback, ): void | null | undefined | iobJS.TypedState | iobJS.AbsentState { - if ((adapter.config as AdapterConfig).subscribe) { + if ((adapter.config as JavaScriptAdapterConfig).subscribe) { if (typeof callback !== 'function') { sandbox.log('You cannot use this function synchronous', 'error'); } else { - adapter.getForeignState(this[0], (err: Error | null, state?: ioBroker.State | null) => - callback( - err, - context.convertBackStringifiedValues(this[0], state) as - | iobJS.TypedState - | iobJS.AbsentState, - ), + adapter.getForeignState( + this[0], + (err: Error | null | undefined, state?: ioBroker.State | null): void => { + callback( + err, + context.convertBackStringifiedValues(this[0], state) as + | iobJS.TypedState + | iobJS.AbsentState, + ); + }, ); } } else { @@ -1060,9 +1077,9 @@ export function sandBox( } }; result.getStateAsync = async function (): Promise< - iobJS.TypedState | iobJS.AbsentState | null + iobJS.TypedState | iobJS.AbsentState | null | undefined > { - if ((adapter.config as AdapterConfig).subscribe) { + if ((adapter.config as JavaScriptAdapterConfig).subscribe) { const state = await adapter.getForeignStateAsync(this[0]); return context.convertBackStringifiedValues(this[0], state) as | iobJS.TypedState @@ -1128,8 +1145,8 @@ export function sandBox( result.setStateDelayed = function ( state: ioBroker.SettableState | ioBroker.StateValue, isAck: boolean | number | undefined, - delay: number | boolean, - clearRunning: boolean | (() => void), + delay?: number | boolean, + clearRunning?: boolean | (() => void), callback?: () => void, ) { if (typeof isAck !== 'boolean') { @@ -1211,7 +1228,11 @@ export function sandBox( 'info', ); - if (sandbox.__engine.__subscriptionsLog % (adapter.config as AdapterConfig).maxTriggersPerScript === 0) { + if ( + sandbox.__engine.__subscriptionsLog % + (adapter.config as JavaScriptAdapterConfig).maxTriggersPerScript === + 0 + ) { sandbox.log( `More than ${sandbox.__engine.__subscriptionsLog} log subscriptions registered. Check your script!`, 'warn', @@ -1221,14 +1242,14 @@ export function sandBox( return handler.id; }, onLogUnregister: function ( - idOrCallbackOrSeverity: string | ioBroker.LogLevel | ((info: LogMessage) => void), + idOrCallbackOrSeverity: number | ioBroker.LogLevel | ((info: LogMessage) => void), ): boolean { let found = false; if (context.logSubscriptions?.[sandbox.scriptName]) { sandbox.verbose && sandbox.log( - `onLogUnregister(idOrCallbackOrSeverity=${idOrCallbackOrSeverity}) - logSubscriptions=${sandbox.__engine.__subscriptionsLog}`, + `onLogUnregister(idOrCallbackOrSeverity=${JSON.stringify(idOrCallbackOrSeverity)}) - logSubscriptions=${sandbox.__engine.__subscriptionsLog}`, 'info', ); @@ -1240,7 +1261,7 @@ export function sandBox( ) { sandbox.verbose && sandbox.log( - `onLogUnregister(idOrCallbackOrSeverity=${idOrCallbackOrSeverity}, removing id=${context.logSubscriptions[sandbox.scriptName][i].id})`, + `onLogUnregister(idOrCallbackOrSeverity=${JSON.stringify(idOrCallbackOrSeverity)}, removing id=${context.logSubscriptions[sandbox.scriptName][i].id})`, 'info', ); @@ -1257,7 +1278,7 @@ export function sandBox( } else { sandbox.verbose && sandbox.log( - `onLogUnregister(idOrCallbackOrSeverity=${idOrCallbackOrSeverity}) NOT = ${JSON.stringify(context.logSubscriptions[sandbox.scriptName][i])}`, + `onLogUnregister(idOrCallbackOrSeverity=${JSON.stringify(idOrCallbackOrSeverity)}) NOT = ${JSON.stringify(context.logSubscriptions[sandbox.scriptName][i])}`, 'info', ); } @@ -1277,7 +1298,7 @@ export function sandBox( callback = options as (error: Error | null | string, stdout?: string, stderr?: string) => void; options = {}; } - if (!(adapter.config as AdapterConfig).enableExec) { + if (!(adapter.config as JavaScriptAdapterConfig).enableExec) { const error = 'exec is not available. Please enable "Enable Exec" option in instance settings'; sandbox.log(error, 'error'); @@ -1285,7 +1306,9 @@ export function sandBox( setImmediate(callback, error, undefined, undefined); } } else { - sandbox.verbose && sandbox.log(`exec(cmd=${cmd})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`exec(cmd=${cmd})`, 'info'); + } if (debug) { sandbox.log(words._('Command %s was not executed, while debug mode is active', cmd), 'warn'); @@ -1302,8 +1325,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, error, stdout, stderr); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }, @@ -1312,12 +1335,16 @@ export function sandBox( } }, email: function (msg: string | SendMailOptions): void { - sandbox.verbose && sandbox.log(`email(msg=${JSON.stringify(msg)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`email(msg=${JSON.stringify(msg)})`, 'info'); + } sandbox.log(`email(msg=${JSON.stringify(msg)}) is deprecated. Please use sendTo instead!`, 'warn'); adapter.sendTo('email', msg); }, pushover: function (msg: string | PushoverOptions): void { - sandbox.verbose && sandbox.log(`pushover(msg=${JSON.stringify(msg)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`pushover(msg=${JSON.stringify(msg)})`, 'info'); + } sandbox.log(`pushover(msg=${JSON.stringify(msg)}) is deprecated. Please use sendTo instead!`, 'warn'); adapter.sendTo('pushover', msg); }, @@ -1369,7 +1396,9 @@ export function sandBox( method: 'get', }; - sandbox.verbose && sandbox.log(`httpGet(config=${JSON.stringify(config)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`httpGet(config=${JSON.stringify(config)})`, 'info'); + } const startTime = Date.now(); @@ -1378,7 +1407,9 @@ export function sandBox( .then((response: AxiosResponse) => { const responseTime = Date.now() - startTime; - sandbox.verbose && sandbox.log(`httpGet(url=${url}, responseTime=${responseTime}ms)`, 'info'); + if (sandbox.verbose) { + sandbox.log(`httpGet(url=${url}, responseTime=${responseTime}ms)`, 'info'); + } if (typeof callback === 'function') { try { @@ -1388,8 +1419,8 @@ export function sandBox( headers: response.headers as Record, responseTime, }); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }) @@ -1422,8 +1453,8 @@ export function sandBox( try { callback.call(sandbox, error.message, result); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }); @@ -1480,7 +1511,9 @@ export function sandBox( data, }; - sandbox.verbose && sandbox.log(`httpPost(config=${JSON.stringify(config)}, data=${data})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`httpPost(config=${JSON.stringify(config)}, data=${data})`, 'info'); + } const startTime = Date.now(); @@ -1489,7 +1522,9 @@ export function sandBox( .then((response: AxiosResponse) => { const responseTime = Date.now() - startTime; - sandbox.verbose && sandbox.log(`httpPost(url=${url}, responseTime=${responseTime}ms)`, 'info'); + if (sandbox.verbose) { + sandbox.log(`httpPost(url=${url}, responseTime=${responseTime}ms)`, 'info'); + } if (typeof callback === 'function') { try { @@ -1499,37 +1534,43 @@ export function sandBox( headers: response.headers, responseTime, }); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }) - .catch(error => { + .catch((error: unknown) => { const responseTime = Date.now() - startTime; - sandbox.log(`httpPost(url=${url}, error=${error.message})`, 'error'); + sandbox.log(`httpPost(url=${url}, error=${(error as Error).message})`, 'error'); if (typeof callback === 'function') { - let result = { + let result: { + statusCode: number | null; + data: any; + headers: Record; + responseTime: number; + } = { statusCode: null, data: null, headers: {}, responseTime, }; + const response: AxiosResponse | undefined = (error as AxiosError).response; - if (error.response) { + if (response) { result = { - statusCode: error.response.status, - data: error.response.data, - headers: error.response.headers, + statusCode: response.status, + data: response.data, + headers: response.headers, responseTime, }; } try { - callback.call(sandbox, error.message, result); - } catch (e) { - errorInCallback(e); + callback.call(sandbox, new Error((error as AxiosError).message), result); + } catch (err: unknown) { + errorInCallback(err as Error); } } }); @@ -1586,6 +1627,7 @@ export function sandBox( | SchedulerRule | string | (TimeRule | AstroRule | Pattern | SchedulerRule | string)[], + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents callbackOrChangeTypeOrId: string | ChangeType | ((event?: EventObj) => void), value?: any, ): @@ -1676,7 +1718,10 @@ export function sandBox( sandbox.__engine.__subscriptions += 1; - if (sandbox.__engine.__subscriptions % (adapter.config as AdapterConfig).maxTriggersPerScript === 0) { + if ( + sandbox.__engine.__subscriptions % (adapter.config as JavaScriptAdapterConfig).maxTriggersPerScript === + 0 + ) { sandbox.log( `More than ${sandbox.__engine.__subscriptions} subscriptions registered. Check your script!`, 'warn', @@ -1697,11 +1742,11 @@ export function sandBox( } else { if (typeof value === 'undefined') { callback = function (obj: EventObj) { - sandbox.setState(callbackOrChangeTypeOrId as string, obj.newState.val); + sandbox.setState(callbackOrChangeTypeOrId, obj.newState.val); }; } else { callback = function (/* obj */) { - sandbox.setState(callbackOrChangeTypeOrId as string, value); + sandbox.setState(callbackOrChangeTypeOrId, value); }; } } @@ -1712,8 +1757,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, obj); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }, @@ -1738,7 +1783,9 @@ export function sandBox( } } } - sandbox.verbose && sandbox.log(`subscribe: ${JSON.stringify(subs)}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`subscribe: ${JSON.stringify(subs)}`, 'info'); + } subscribePattern(script, oPattern.id as string); @@ -1760,7 +1807,9 @@ export function sandBox( pattern: context.subscriptions[s].pattern, }); } - sandbox.verbose && sandbox.log(`getSubscriptions() => ${JSON.stringify(result)}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`getSubscriptions() => ${JSON.stringify(result)}`, 'info'); + } return result; }, getFileSubscriptions: function (): Record { @@ -1774,7 +1823,9 @@ export function sandBox( fileNamePattern: context.subscriptionsFile[s].fileNamePattern, }); } - sandbox.verbose && sandbox.log(`getFileSubscriptions() => ${JSON.stringify(result)}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`getFileSubscriptions() => ${JSON.stringify(result)}`, 'info'); + } return result; }, adapterSubscribe: function (id: string): void { @@ -1789,7 +1840,9 @@ export function sandBox( const alive = `system.adapter.${a}.alive`; context.adapterSubs[alive] = context.adapterSubs[alive] || []; context.adapterSubs[alive].push(id); - sandbox.verbose && sandbox.log(`adapterSubscribe: ${a} - ${id}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`adapterSubscribe: ${a} - ${id}`, 'info'); + } adapter.sendTo(a, 'subscribe', id); } }, @@ -1810,7 +1863,9 @@ export function sandBox( return result; } - sandbox.verbose && sandbox.log(`adapterUnsubscribe(id=${JSON.stringify(idOrObject)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`adapterUnsubscribe(id=${JSON.stringify(idOrObject)})`, 'info'); + } if (isObject(idOrObject)) { for (let i = context.subscriptions.length - 1; i >= 0; i--) { @@ -1842,6 +1897,7 @@ export function sandBox( | SchedulerRule | string | (TimeRule | AstroRule | Pattern | SchedulerRule | string)[], + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents callbackOrChangeTypeOrId: string | ChangeType | ((event?: EventObj) => void), value?: any, ): @@ -1857,9 +1913,9 @@ export function sandBox( if (enums.includes(enumId)) { const subscriptions: Record = {}; - const init = () => { + const init = (): void => { const obj: ioBroker.EnumObject = objects[enumId] as ioBroker.EnumObject; - const common: ioBroker.EnumCommon = (obj?.common ?? {}) as ioBroker.EnumCommon; + const common: ioBroker.EnumCommon = obj?.common ?? {}; const members: string[] = common?.members ?? []; // Remove old subscriptions @@ -1902,13 +1958,19 @@ export function sandBox( withFileOrCallback: | boolean | ((id: string, fileName: string, size: number, file?: string | Buffer, mimeType?: string) => void), - callback?: (id: string, fileName: string, size: number, file?: string | Buffer, mimeType?: string) => void, + callback?: ( + id: string, + fileName: string, + size: number | null, + file?: string | Buffer, + mimeType?: string, + ) => void, ): undefined | FileSubscriptionResult | (undefined | FileSubscriptionResult)[] { if (typeof withFileOrCallback === 'function') { callback = withFileOrCallback as ( id: string, fileName: string, - size: number, + size: number | null, file?: string | Buffer, mimeType?: string, ) => void; @@ -1951,7 +2013,11 @@ export function sandBox( 'info', ); - if (sandbox.__engine.__subscriptionsFile % (adapter.config as AdapterConfig).maxTriggersPerScript === 0) { + if ( + sandbox.__engine.__subscriptionsFile % + (adapter.config as JavaScriptAdapterConfig).maxTriggersPerScript === + 0 + ) { sandbox.log( `More than ${sandbox.__engine.__subscriptionsFile} file subscriptions registered. Check your script!`, 'warn', @@ -1970,30 +2036,30 @@ export function sandBox( const subs: FileSubscriptionResult = { id, fileNamePattern, - withFile: withFileOrCallback as boolean, + withFile: withFileOrCallback, idRegEx, fileRegEx, - callback: (id: string, fileName: string, size: number, withFile: boolean): void => { + callback: (id: string, fileName: string, size: number | null, withFile: boolean): void => { try { sandbox.verbose && sandbox.log(`onFile changed(id=${id}, fileName=${fileName}, size=${size})`, 'info'); - if (withFile && size > 0) { + if (withFile && (size || 0) > 0) { adapter .readFileAsync(id, fileName) .then(data => { try { callback.call(sandbox, id, fileName, size, data.file, data.mimeType); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }) .catch(error => errorInCallback(error)); } else { callback.call(sandbox, id, fileName, size); } - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }, name, @@ -2017,7 +2083,7 @@ export function sandBox( sandbox.verbose && sandbox.log( - `offFile(idOrObject=${JSON.stringify(idOrObject)}, fileNamePattern=${fileNamePattern}) - fileSubscriptions=${sandbox.__engine.__subscriptionsFile}`, + `offFile(idOrObject=${JSON.stringify(idOrObject)}, fileNamePattern=${JSON.stringify(fileNamePattern)}) - fileSubscriptions=${sandbox.__engine.__subscriptionsFile}`, 'info', ); @@ -2025,7 +2091,7 @@ export function sandBox( if (Array.isArray(idOrObject)) { const result: boolean[] = []; for (let t = 0; t < idOrObject.length; t++) { - result.push(sandbox.offFile(idOrObject[t] as FileSubscriptionResult | string) as boolean); + result.push(sandbox.offFile(idOrObject[t]) as boolean); } return result; } @@ -2039,7 +2105,7 @@ export function sandBox( sandbox.verbose && sandbox.log( - `offFile(type=object, fileNamePattern=${fileNamePattern}, removing id=${context.subscriptionsFile[i].id})`, + `offFile(type=object, fileNamePattern=${JSON.stringify(fileNamePattern)}, removing id=${context.subscriptionsFile[i].id})`, 'info', ); @@ -2051,7 +2117,7 @@ export function sandBox( return false; } - if (isArray(fileNamePattern)) { + if (fileNamePattern && Array.isArray(fileNamePattern)) { const result: boolean[] = []; for (let t = 0; t < fileNamePattern.length; t++) { result.push(sandbox.offFile(idOrObject, fileNamePattern[t]) as boolean); @@ -2096,6 +2162,7 @@ export function sandBox( callback?: (event?: EventObj) => void, ): string | SubscriptionResult | Promise { function _once(cb: (obj?: EventObj) => void): string | SubscriptionResult { + // eslint-disable-next-line prefer-const let subscription: string | SubscriptionResult; const handler = (obj?: EventObj): void => { subscription && sandbox.unsubscribe(subscription); @@ -2131,6 +2198,14 @@ export function sandBox( 'info', ); + if (!context.scheduler) { + sandbox.log( + `Cannot schedule "${typeof pattern === 'object' ? JSON.stringify(pattern) : pattern}" because scheduler is not available`, + 'error', + ); + return null; + } + const schedule: string | null = context.scheduler.add( pattern as SchedulerRule | string, sandbox.scriptName, @@ -2140,7 +2215,11 @@ export function sandBox( script.wizards.push(schedule); sandbox.__engine.__schedules += 1; - if (sandbox.__engine.__schedules % (adapter.config as AdapterConfig).maxTriggersPerScript === 0) { + if ( + sandbox.__engine.__schedules % + (adapter.config as JavaScriptAdapterConfig).maxTriggersPerScript === + 0 + ) { sandbox.log( `More than ${sandbox.__engine.__schedules} schedules registered. Check your script!`, 'warn', @@ -2151,17 +2230,17 @@ export function sandBox( return schedule; } + const adapterConfig: JavaScriptAdapterConfig = adapter.config as JavaScriptAdapterConfig; + if (typeof pattern === 'object' && (pattern as AstroRule).astro) { const astroPattern = pattern as AstroRule; - const nowdate = new Date(); + const nowDate = new Date(); if ( - (adapter.config as AdapterConfig).latitude === undefined || - (adapter.config as AdapterConfig).longitude === undefined || - (adapter.config as AdapterConfig).latitude === '' || - (adapter.config as AdapterConfig).longitude === '' || - (adapter.config as AdapterConfig).latitude === null || - (adapter.config as AdapterConfig).longitude === null + adapterConfig.latitude === undefined || + adapterConfig.longitude === undefined || + adapterConfig.latitude === null || + adapterConfig.longitude === null ) { sandbox.log('Longitude or latitude does not set. Cannot use astro.', 'error'); return null; @@ -2169,30 +2248,26 @@ export function sandBox( // ensure events are calculated independent of current time // TODO: use getAstroStartOfDay of adapter? - const todayNoon = new Date(nowdate); + const todayNoon = new Date(nowDate); todayNoon.setHours(12, 0, 0, 0); - let ts = mods.suncalc.getTimes( - todayNoon, - (adapter.config as AdapterConfig).latitude, - (adapter.config as AdapterConfig).longitude, - )[astroPattern.astro]; + let ts = mods.suncalc.getTimes(todayNoon, adapterConfig.latitude, adapterConfig.longitude)[ + astroPattern.astro + ]; // event on the next day, correct or force recalculation at midnight if (todayNoon.getDate() !== ts.getDate()) { todayNoon.setDate(todayNoon.getDate() - 1); - ts = mods.suncalc.getTimes( - todayNoon, - (adapter.config as AdapterConfig).latitude, - (adapter.config as AdapterConfig).longitude, - )[astroPattern.astro]; + ts = mods.suncalc.getTimes(todayNoon, adapterConfig.latitude, adapterConfig.longitude)[ + astroPattern.astro + ]; } if (ts.getTime().toString() === 'NaN') { sandbox.log( - `Cannot calculate "${astroPattern.astro}" for ${(adapter.config as AdapterConfig).latitude}, ${(adapter.config as AdapterConfig).longitude}`, + `Cannot calculate "${astroPattern.astro}" for ${adapterConfig.latitude}, ${adapterConfig.longitude}`, 'warn', ); - ts = new Date(nowdate.getTime()); + ts = new Date(nowDate.getTime()); if ( astroPattern.astro === 'sunriseEnd' || @@ -2215,8 +2290,8 @@ export function sandBox( ts = new Date(ts.getTime() + astroPattern.shift * 60000); } - if (!ts || ts < nowdate) { - const date = new Date(nowdate); + if (!ts || ts < nowDate) { + const date = new Date(nowDate); // Event doesn't occur today - try again tomorrow // Calculate time till 24:00 (local, NOT UTC) and set timeout date.setDate(date.getDate() + 1); @@ -2227,7 +2302,7 @@ export function sandBox( sandbox.__engine.__schedules += 1; - if (sandbox.__engine.__schedules % (adapter.config as AdapterConfig).maxTriggersPerScript === 0) { + if (sandbox.__engine.__schedules % adapterConfig.maxTriggersPerScript === 0) { sandbox.log( `More than ${sandbox.__engine.__schedules} schedules registered. Check your script!`, 'warn', @@ -2253,7 +2328,7 @@ export function sandBox( sandbox.__engine.__schedules += 1; - if (sandbox.__engine.__schedules % (adapter.config as AdapterConfig).maxTriggersPerScript === 0) { + if (sandbox.__engine.__schedules % adapterConfig.maxTriggersPerScript === 0) { sandbox.log( `More than ${sandbox.__engine.__schedules} schedules registered. Check your script!`, 'warn', @@ -2263,8 +2338,8 @@ export function sandBox( sandbox.setTimeout(() => { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } // Reschedule in 2 seconds sandbox.setTimeout(() => { @@ -2299,14 +2374,18 @@ export function sandBox( const schedule: IobSchedule = mods.nodeSchedule.scheduleJob(pattern, (): void => { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }); if (schedule) { sandbox.__engine.__schedules += 1; - if (sandbox.__engine.__schedules % (adapter.config as AdapterConfig).maxTriggersPerScript === 0) { + if ( + sandbox.__engine.__schedules % + (adapter.config as JavaScriptAdapterConfig).maxTriggersPerScript === + 0 + ) { sandbox.log( `More than ${sandbox.__engine.__schedules} schedules registered. Check your script!`, 'warn', @@ -2322,10 +2401,12 @@ export function sandBox( script.schedules.push(schedule); } else { - sandbox.log(`schedule(cron=${pattern}): cannot create schedule`, 'error'); + sandbox.log(`schedule(cron=${JSON.stringify(pattern)}): cannot create schedule`, 'error'); } - sandbox.verbose && sandbox.log(`schedule(cron=${pattern})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`schedule(cron=${JSON.stringify(pattern)})`, 'info'); + } return schedule; } @@ -2350,18 +2431,16 @@ export function sandBox( let isValid = false; - if (rhms.test(time)) { - [h, m, s] = time - .match(rhms) - ?.slice(1) - .map(v => parseInt(v)); - isValid = true; - } else if (rhm.test(time)) { - [h, m] = time - .match(rhm) - ?.slice(1) - .map(v => parseInt(v)); + let result = time.match(rhms); + if (result) { + [, h, m, s] = result.map(v => parseInt(v)); isValid = true; + } else { + result = time.match(rhm); + if (result) { + [, h, m] = result.map(v => parseInt(v)); + isValid = true; + } } if (isValid) { @@ -2384,8 +2463,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }); @@ -2406,14 +2485,16 @@ export function sandBox( sandbox.getState(id, (err, state) => { if (!err && state?.val) { - sandbox.verbose && sandbox.log(`scheduleById(id=${id}): Init with value ${state.val}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`scheduleById(id=${id}): Init with value ${state.val}`, 'info'); + } init(state.val.toString()); } }); const triggerDef: Pattern = { id, change: 'any' }; if (ack !== undefined) { - triggerDef.ack = ack as boolean; + triggerDef.ack = ack; } sandbox.on(triggerDef, obj => { @@ -2442,10 +2523,10 @@ export function sandBox( } if ( - (!(adapter.config as AdapterConfig).latitude && - ((adapter.config as AdapterConfig).latitude as unknown as number) !== 0) || - (!(adapter.config as AdapterConfig).longitude && - ((adapter.config as AdapterConfig).longitude as unknown as number) !== 0) + (!(adapter.config as JavaScriptAdapterConfig).latitude && + ((adapter.config as JavaScriptAdapterConfig).latitude as unknown as number) !== 0) || + (!(adapter.config as JavaScriptAdapterConfig).longitude && + ((adapter.config as JavaScriptAdapterConfig).longitude as unknown as number) !== 0) ) { sandbox.log('Longitude or latitude does not set. Cannot use astro.', 'error'); return; @@ -2455,18 +2536,20 @@ export function sandBox( date.setHours(12, 0, 0, 0); let ts = mods.suncalc.getTimes( date, - (adapter.config as AdapterConfig).latitude, - (adapter.config as AdapterConfig).longitude, + (adapter.config as JavaScriptAdapterConfig).latitude, + (adapter.config as JavaScriptAdapterConfig).longitude, )[pattern]; if (ts === undefined || ts.getTime().toString() === 'NaN') { sandbox.log( - `Cannot calculate astro date "${pattern}" for ${(adapter.config as AdapterConfig).latitude}, ${(adapter.config as AdapterConfig).longitude}`, + `Cannot calculate astro date "${pattern}" for ${(adapter.config as JavaScriptAdapterConfig).latitude}, ${(adapter.config as JavaScriptAdapterConfig).longitude}`, 'warn', ); } - sandbox.verbose && sandbox.log(`getAstroDate(pattern=${pattern}, date=${date}) => ${ts}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`getAstroDate(pattern=${pattern}, date=${date.toString()}) => ${ts}`, 'info'); + } if (offsetMinutes !== undefined) { ts = new Date(ts.getTime() + offsetMinutes * 60000); @@ -2482,13 +2565,17 @@ export function sandBox( return; } - sandbox.verbose && sandbox.log(`isAstroDay() => ${nowDate >= dayBegin && nowDate <= dayEnd}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`isAstroDay() => ${nowDate >= dayBegin && nowDate <= dayEnd}`, 'info'); + } return nowDate >= dayBegin && nowDate <= dayEnd; }, clearSchedule: function (schedule: IobSchedule | ScheduleName | string): boolean { - if (context.scheduler.get(schedule as string | ScheduleName)) { - sandbox.verbose && sandbox.log('clearSchedule() => wizard cleared', 'info'); + if (context.scheduler?.get(schedule as string | ScheduleName)) { + if (sandbox.verbose) { + sandbox.log('clearSchedule() => wizard cleared', 'info'); + } const pos = script.wizards.indexOf(schedule as string); if (pos !== -1) { script.wizards.splice(pos, 1); @@ -2505,35 +2592,39 @@ export function sandBox( if (!mods.nodeSchedule.cancelJob(script.schedules[i])) { sandbox.log('Error by canceling scheduled job', 'error'); } - delete script.schedules[i]; script.schedules.splice(i, 1); if (sandbox.__engine.__schedules > 0) { sandbox.__engine.__schedules--; } - sandbox.verbose && sandbox.log('clearSchedule() => cleared', 'info'); + if (sandbox.verbose) { + sandbox.log('clearSchedule() => cleared', 'info'); + } return true; } } else if (script.schedules[i] === schedule) { if (!mods.nodeSchedule.cancelJob(script.schedules[i])) { sandbox.log('Error by canceling scheduled job', 'error'); } - delete script.schedules[i]; script.schedules.splice(i, 1); if (sandbox.__engine.__schedules > 0) { sandbox.__engine.__schedules--; } - sandbox.verbose && sandbox.log('clearSchedule() => cleared', 'info'); + if (sandbox.verbose) { + sandbox.log('clearSchedule() => cleared', 'info'); + } return true; } } - sandbox.verbose && sandbox.log('clearSchedule() => invalid handler', 'warn'); + if (sandbox.verbose) { + sandbox.log('clearSchedule() => invalid handler', 'warn'); + } return false; }, getSchedules: function (allScripts?: boolean): ScheduleName[] { - const schedules = context.scheduler.getList(); + const schedules = context.scheduler?.getList() || []; if (allScripts) { Object.keys(context.scripts).forEach( name => @@ -2610,12 +2701,14 @@ export function sandBox( } delete timers[id]; } else { - sandbox.verbose && sandbox.log('setStateDelayed: no running timers', 'info'); + if (sandbox.verbose) { + sandbox.log('setStateDelayed: no running timers', 'info'); + } } } // If no delay => starts immediately if (!delay) { - sandbox.setState(id, state, isAck as boolean | undefined, callback); + sandbox.setState(id, state, isAck, callback); return null; } // If delay @@ -2630,7 +2723,7 @@ export function sandBox( // Start timeout const timer = setTimeout( function (_timerId, _id, _state, _isAck) { - sandbox.setState(_id, _state, _isAck as boolean | undefined, callback); + sandbox.setState(_id, _state, _isAck, callback); // delete timer handler if (timers[_id]) { // optimisation @@ -2671,7 +2764,7 @@ export function sandBox( (state as ioBroker.SettableState).val !== undefined && (state as ioBroker.SettableState).ack !== undefined ? (state as ioBroker.SettableState).ack - : (isAck as boolean | undefined), + : isAck, }); return context.timerId; @@ -2682,7 +2775,9 @@ export function sandBox( id = `${adapter.namespace}.${id}`; } - sandbox.verbose && sandbox.log(`clearStateDelayed(id=${id}, timerId=${timerId})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`clearStateDelayed(id=${id}, timerId=${timerId})`, 'info'); + } if (timers[id]) { for (let i = timers[id].length - 1; i >= 0; i--) { @@ -2691,7 +2786,9 @@ export function sandBox( if (timerId !== undefined) { timers[id].splice(i, 1); } - sandbox.verbose && sandbox.log(`clearStateDelayed: clear timer ${timers[id][i].id}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`clearStateDelayed: clear timer ${timers[id][i].id}`, 'info'); + } } } if (timerId === undefined) { @@ -2741,7 +2838,7 @@ export function sandBox( return null; } - let result: { + const result: { timerId: number; left: number; delay: number; @@ -2761,7 +2858,7 @@ export function sandBox( } return result; } - let result: Record< + const result: Record< string, { timerId: number; left: number; delay: number; val: ioBroker.StateValue; ack?: boolean }[] > = {}; @@ -2810,8 +2907,8 @@ export function sandBox( }, getState: function ( id: string, - callback?: (err: Error | null | undefined, state?: ioBroker.State | null | undefined) => void, - ): undefined | void | (ioBroker.State & { notExist?: true }) { + callback?: (err: Error | null | undefined, state?: ioBroker.State | null) => void, + ): undefined | void | (ioBroker.State & { notExist?: true }) | null { if (typeof id !== 'string') { sandbox.log(`getState has been called with id of type "${typeof id}" but expects a string`, 'error'); return undefined; @@ -2828,7 +2925,7 @@ export function sandBox( ); } } else { - if ((adapter.config as AdapterConfig).subscribe) { + if ((adapter.config as JavaScriptAdapterConfig).subscribe) { sandbox.log( 'The "getState" method cannot be used synchronously, because the adapter setting "Do not subscribe to all states on start" is enabled.', 'error', @@ -2841,7 +2938,7 @@ export function sandBox( if (states[id]) { sandbox.verbose && sandbox.log( - `getState(id=${id}, timerId=${timers[id]}) => ${JSON.stringify(states[id])}`, + `getState(id=${id}, timerId=${JSON.stringify(timers[id])}) => ${JSON.stringify(states[id])}`, 'info', ); if (context.interimStateValues[id] !== undefined) { @@ -2851,7 +2948,7 @@ export function sandBox( } else if (states[`${adapter.namespace}.${id}`]) { sandbox.verbose && sandbox.log( - `getState(id=${id}, timerId=${timers[id]}) => ${states[`${adapter.namespace}.${id}`]}`, + `getState(id=${id}, timerId=${JSON.stringify(timers[id])}) => ${JSON.stringify(states[`${adapter.namespace}.${id}`])}`, 'info', ); if (context.interimStateValues[`${adapter.namespace}.${id}`] !== undefined) { @@ -2863,10 +2960,12 @@ export function sandBox( return context.convertBackStringifiedValues(id, states[`${adapter.namespace}.${id}`]); } - sandbox.verbose && sandbox.log(`getState(id=${id}, timerId=${timers[id]}) => not found`, 'info'); + if (sandbox.verbose) { + sandbox.log(`getState(id=${id}, timerId=${JSON.stringify(timers[id])}) => not found`, 'info'); + } - context.logWithLineInfo?.warn( - `getState "${id}" not found (3)${states[id] !== undefined ? ` states[id]=${states[id]}` : ''}`, + context.logWithLineInfo( + `getState "${id}" not found (3)${states[id] !== undefined ? ` states[id]=${JSON.stringify(states[id])}` : ''}`, ); ///xxx return { val: null, notExist: true } as ioBroker.State & { notExist?: true }; } @@ -2888,7 +2987,7 @@ export function sandBox( return; } - if ((adapter.config as AdapterConfig).subscribe) { + if ((adapter.config as JavaScriptAdapterConfig).subscribe) { adapter.getForeignState(id, (err, state) => { callback(err, !!state); }); @@ -2897,7 +2996,7 @@ export function sandBox( } }); } else { - if ((adapter.config as AdapterConfig).subscribe) { + if ((adapter.config as JavaScriptAdapterConfig).subscribe) { sandbox.log( 'The "existsState" method cannot be used synchronously, because the adapter setting "Do not subscribe to all states on start" is enabled.', 'error', @@ -2948,11 +3047,8 @@ export function sandBox( }, getObject: function ( id: string, - enumName: - | null - | string - | ((err: Error | null | undefined, obj?: ioBroker.Object | null | undefined) => void), - cb?: (err: Error | null | undefined, obj?: ioBroker.Object | null | undefined) => void, + enumName: null | string | ((err: Error | null | undefined, obj?: ioBroker.Object | null) => void), + cb?: (err: Error | null | undefined, obj?: ioBroker.Object | null) => void, ): void | ioBroker.Object | null { if (typeof id !== 'string') { sandbox.log(`getObject has been called with id of type "${typeof id}" but expects a string`, 'error'); @@ -2974,7 +3070,7 @@ export function sandBox( let result: ioBroker.Object | null | undefined; try { result = JSON.parse(JSON.stringify(objects[id])); - } catch (err) { + } catch (err: unknown) { adapter.setState(`scriptProblem.${name.substring('script.js.'.length)}`, { val: true, ack: true, @@ -3016,7 +3112,7 @@ export function sandBox( let result: ioBroker.Object | null | undefined; try { result = JSON.parse(JSON.stringify(objects[id])); - } catch (err) { + } catch (err: unknown) { adapter.setState(`scriptProblem.${name.substring('script.js.'.length)}`, { val: true, ack: true, @@ -3034,7 +3130,7 @@ export function sandBox( setObject: function ( _id: string, _obj: ioBroker.Object, - callback?: (err?: Error | null | undefined, res?: { id: string }) => void, + callback?: (err?: Error | null, res?: { id: string }) => void, ): void { sandbox.log('Function "setObject" is not allowed. Use adapter settings to allow it.', 'error'); if (typeof callback === 'function') { @@ -3043,8 +3139,8 @@ export function sandBox( sandbox, new Error('Function "setObject" is not allowed. Use adapter settings to allow it.'), ); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }, @@ -3052,7 +3148,7 @@ export function sandBox( extendObject: function ( _id: string, _obj: Partial, - callback?: (err?: Error | null | undefined, res?: { id: string }) => void, + callback?: (err?: Error | null, res?: { id: string }) => void, ): void { sandbox.log('Function "extendObject" is not allowed. Use adapter settings to allow it.', 'error'); if (typeof callback === 'function') { @@ -3061,8 +3157,8 @@ export function sandBox( sandbox, new Error('Function "extendObject" is not allowed. Use adapter settings to allow it.'), ); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }, @@ -3082,8 +3178,8 @@ export function sandBox( sandbox, new Error('Function "deleteObject" is not allowed. Use adapter settings to allow it.'), ); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }, @@ -3101,14 +3197,16 @@ export function sandBox( }); } } - sandbox.verbose && sandbox.log(`getEnums(enumName=${enumName}) => ${JSON.stringify(result)}`, 'info'); + if (sandbox.verbose) { + sandbox.log(`getEnums(enumName=${enumName}) => ${JSON.stringify(result)}`, 'info'); + } return JSON.parse(JSON.stringify(result)); }, createAlias: function ( name: string, alias: string | CommonAlias, forceCreation: boolean | Partial | ((err: Error | null) => void) | undefined, - common?: Partial | undefined | Record | ((err: Error | null) => void), + common?: Partial | Record | ((err: Error | null) => void), native?: Record | ((err: Error | null) => void), callback?: (err: Error | null) => void, ) { @@ -3136,8 +3234,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; @@ -3149,8 +3247,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; @@ -3176,32 +3274,35 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; } - let aliasSourceId: string = isObject(_common.alias.id) - ? (_common.alias.id as { read: string; write: string }).read - : (_common.alias.id as string); - if (!objects[aliasSourceId] && objects[`${adapter.namespace}.${aliasSourceId}`]) { - aliasSourceId = `${adapter.namespace}.${aliasSourceId}`; - if (isObject(_common.alias.id)) { - (_common.alias.id as { read: string; write: string }).read = aliasSourceId; - } else { - _common.alias.id = aliasSourceId; + let aliasSourceId = ''; + if (_common.alias) { + aliasSourceId = isObject(_common.alias.id) + ? (_common.alias.id as { read: string; write: string }).read + : (_common.alias.id as string); + if (!objects[aliasSourceId] && objects[`${adapter.namespace}.${aliasSourceId}`]) { + aliasSourceId = `${adapter.namespace}.${aliasSourceId}`; + if (isObject(_common.alias.id)) { + (_common.alias.id as { read: string; write: string }).read = aliasSourceId; + } else { + _common.alias.id = aliasSourceId; + } + } + if ( + isObject(_common.alias.id) && + (_common.alias.id as { read: string; write: string }).write && + !objects[(_common.alias.id as { read: string; write: string }).write] && + objects[`${adapter.namespace}.${(_common.alias.id as { read: string; write: string }).write}`] + ) { + (_common.alias.id as { read: string; write: string }).write = + `${adapter.namespace}.${(_common.alias.id as { read: string; write: string }).write}`; } - } - if ( - isObject(_common.alias.id) && - (_common.alias.id as { read: string; write: string }).write && - !objects[(_common.alias.id as { read: string; write: string }).write] && - objects[`${adapter.namespace}.${(_common.alias.id as { read: string; write: string }).write}`] - ) { - (_common.alias.id as { read: string; write: string }).write = - `${adapter.namespace}.${(_common.alias.id as { read: string; write: string }).write}`; } const obj = objects[aliasSourceId]; if (!obj) { @@ -3210,8 +3311,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; @@ -3222,8 +3323,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; @@ -3259,7 +3360,7 @@ export function sandBox( forceCreation as boolean, _common, native, - callback as (err: Error | null) => void, + callback as (err?: Error | null) => void, ); }, createState: async function ( @@ -3273,22 +3374,22 @@ export function sandBox( | ((err: Error | null) => void), common?: Partial | ((err: Error | null) => void), native?: Record | ((err: Error | null) => void), - callback?: (error?: Error | null) => void, + callback?: (error: Error | null | undefined, id?: string) => void, ) { if (typeof native === 'function') { - callback = native as (err: Error | null) => void; + callback = native as (err?: Error | null) => void; native = {}; } if (typeof common === 'function') { - callback = common as (err: Error | null) => void; + callback = common as (err?: Error | null) => void; common = undefined; } if (typeof initValue === 'function') { - callback = initValue as (err: Error | null) => void; + callback = initValue as (err?: Error | null) => void; initValue = undefined; } if (typeof forceCreation === 'function') { - callback = forceCreation as (err: Error | null) => void; + callback = forceCreation as (err?: Error | null) => void; forceCreation = undefined; } if (isObject(initValue)) { @@ -3309,8 +3410,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; @@ -3322,8 +3423,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; @@ -3356,15 +3457,14 @@ export function sandBox( sandbox.log(err, 'error'); if (typeof callback === 'function') { try { - callback.call(sandbox, err); - } catch (e) { - errorInCallback(e); + callback.call(sandbox, new Error(err)); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; - } else { - _common.min = min; } + _common.min = min; } } if (_common.max !== undefined) { @@ -3377,14 +3477,13 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; - } else { - _common.max = max; } + _common.max = max; } } @@ -3401,14 +3500,13 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, new Error(err)); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; - } else { - _common.def = def; } + _common.def = def; } } } @@ -3425,11 +3523,12 @@ export function sandBox( } } - sandbox.verbose && + if (sandbox.verbose) { sandbox.log( - `createState(name=${name}, initValue=${initValue}, forceCreation=${forceCreation}, common=${JSON.stringify(common)}, native=${JSON.stringify(native)}, isAlias=${isAlias})`, + `createState(name=${name}, initValue=${JSON.stringify(initValue)}, forceCreation=${JSON.stringify(forceCreation)}, common=${JSON.stringify(common)}, native=${JSON.stringify(native)}, isAlias=${isAlias})`, 'debug', ); + } let id = `${adapter.namespace}.${name}`; if (name.match(/^javascript\.\d+\./) || name.startsWith('0_userdata.0.') || isAlias) { @@ -3471,7 +3570,7 @@ export function sandBox( delete _common.alias; if (!(alias.id as string).startsWith('alias.0.')) { - alias.id = `alias.0.${alias.id}`; + alias.id = `alias.0.${alias.id as string}`; } let aObj: ioBroker.StateObject | null | undefined; @@ -3480,7 +3579,7 @@ export function sandBox( | ioBroker.StateObject | null | undefined; - } catch (e) { + } catch { // ignore } if (!aObj) { @@ -3505,8 +3604,8 @@ export function sandBox( }; await adapter.setForeignObjectAsync(alias.id as string, _obj); - } catch (e) { - sandbox.log(`Cannot create alias "${alias.id}": ${e}`, 'error'); + } catch (err: unknown) { + sandbox.log(`Cannot create alias "${alias.id as string}": ${err as Error}`, 'error'); } } } else if (isAlias && _common.alias) { @@ -3516,7 +3615,8 @@ export function sandBox( }; } const readId = typeof _common.alias.id === 'string' ? _common.alias.id : _common.alias.id.read; - let writeId = typeof _common.alias.id === 'string' ? _common.alias.id : _common.alias.id.write; + let writeId: string | undefined = + typeof _common.alias.id === 'string' ? _common.alias.id : _common.alias.id.write; if (writeId === readId) { writeId = undefined; } @@ -3524,7 +3624,7 @@ export function sandBox( let aObj: ioBroker.StateObject | null | undefined; try { aObj = (await adapter.getForeignObjectAsync(readId)) as ioBroker.StateObject | null | undefined; - } catch (e) { + } catch { // ignore } if (!aObj) { @@ -3541,8 +3641,8 @@ export function sandBox( }, native: {}, }); - } catch (e) { - sandbox.log(`Cannot create alias "${readId}": ${e}`, 'error'); + } catch (err: unknown) { + sandbox.log(`Cannot create alias "${readId}": ${err as Error}`, 'error'); } } if (writeId && _common.write !== false) { @@ -3551,7 +3651,7 @@ export function sandBox( | ioBroker.StateObject | null | undefined; - } catch (e) { + } catch { // ignore } if (!aObj) { @@ -3568,8 +3668,8 @@ export function sandBox( }, native: {}, }); - } catch (e) { - sandbox.log(`Cannot create alias "${writeId}": ${e}`, 'error'); + } catch (err: unknown) { + sandbox.log(`Cannot create alias "${writeId}": ${err as Error}`, 'error'); } } } @@ -3578,7 +3678,7 @@ export function sandBox( let obj: ioBroker.Object | null | undefined; try { obj = await adapter.getForeignObjectAsync(id); - } catch (err) { + } catch { // ignore } @@ -3603,13 +3703,13 @@ export function sandBox( }; try { await adapter.setForeignObjectAsync(id, newObj); - } catch (err) { - sandbox.log(`Cannot set object "${id}": ${err}`, 'warn'); + } catch (err: unknown) { + sandbox.log(`Cannot set object "${id}": ${err as Error}`, 'warn'); if (typeof callback === 'function') { try { - callback.call(sandbox, err); - } catch (e) { - errorInCallback(e); + callback.call(sandbox, err as Error); + } catch (err: unknown) { + errorInCallback(err as Error); } } return; @@ -3628,29 +3728,32 @@ export function sandBox( setStateHelper(sandbox, true, false, id, null, callback); } else if (isAlias) { try { - states[id] = await adapter.getForeignStateAsync(id); - } catch (err) { + const state = await adapter.getForeignStateAsync(id); + if (state) { + states[id] = state; + } + } catch { // ignore } if (typeof callback === 'function') { try { callback.call(sandbox, null, id); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } } else if (typeof callback === 'function') { try { callback.call(sandbox, null, id); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } await ensureObjectStructure(id); } else { // state yet exists if ( - !(adapter.config as AdapterConfig).subscribe && + !(adapter.config as JavaScriptAdapterConfig).subscribe && !states[id] && states[`${adapter.namespace}.${id}`] === undefined ) { @@ -3666,8 +3769,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, null, id); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } @@ -3678,7 +3781,9 @@ export function sandBox( // todo: check rights // todo: also remove from "names" - sandbox.verbose && sandbox.log(`deleteState(id=${id})`, 'debug'); + if (sandbox.verbose) { + sandbox.log(`deleteState(id=${id})`, 'debug'); + } let found = false; if ((id.startsWith('0_userdata.0.') || id.startsWith(adapter.namespace)) && objects[id]) { @@ -3696,8 +3801,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, err, found); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }); @@ -3717,8 +3822,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, err, found); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }); @@ -3728,9 +3833,9 @@ export function sandBox( sandbox.log(`Cannot delete state "${id}": ${err}`, 'error'); if (typeof callback === 'function') { try { - callback.call(sandbox, err, found); - } catch (e) { - errorInCallback(e); + callback.call(sandbox, new Error(err), found); + } catch (err: unknown) { + errorInCallback(err as Error); } } } @@ -3756,19 +3861,44 @@ export function sandBox( timeout = setTimeout(() => { timeout = null; - sandbox.verbose && sandbox.log(`sendTo => timeout: ${timeoutDuration}`, 'debug'); + if (sandbox.verbose) { + sandbox.log(`sendTo => timeout: ${timeoutDuration}`, 'debug'); + } if (typeof callback === 'function') { try { - callback.call(sandbox, { error: 'timeout' }, options, _adapter); - } catch (e) { - errorInCallback(e); + callback.call(sandbox, { error: 'timeout' }, options as Record, _adapter); + } catch (err: unknown) { + errorInCallback(err as Error); } - callback = null; + callback = undefined; } }, timeoutDuration); } + let cbFunc: undefined | ((result: any) => void); + if (timeout) { + cbFunc = function (result: any): void { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + if (sandbox.verbose && result) { + sandbox.log(`sendTo => ${JSON.stringify(result)}`, 'debug'); + } + + if (typeof callback === 'function') { + try { + callback.call(sandbox, result, options as Record, _adapter); + } catch (err: unknown) { + errorInCallback(err as Error); + } + callback = undefined; + } + }; + } + // If specific instance if (_adapter.match(/\.[0-9]+$/)) { sandbox.verbose && @@ -3776,27 +3906,8 @@ export function sandBox( `sendTo(instance=${_adapter}, cmd=${cmd}, msg=${JSON.stringify(msg)}, hasCallback=${typeof callback === 'function'})`, 'info', ); - adapter.sendTo( - _adapter, - cmd, - msg, - timeout && - function (result: any) { - timeout && clearTimeout(timeout); - - sandbox.verbose && result && sandbox.log(`sendTo => ${JSON.stringify(result)}`, 'debug'); - if (typeof callback === 'function') { - try { - callback.call(sandbox, result, options, _adapter); - } catch (e) { - errorInCallback(e); - } - callback = null; - } - }, - options, - ); + adapter.sendTo(_adapter, cmd, msg, cbFunc, options); } else { // Send it to all instances context.adapter.getObjectView( @@ -3806,7 +3917,7 @@ export function sandBox( options, (err, res) => { if (err || !res) { - sandbox.log(`sendTo failed: ${err.message}`, 'error'); + sandbox.log(`sendTo failed: ${err?.message}`, 'error'); return; } @@ -3818,29 +3929,7 @@ export function sandBox( `sendTo(instance=${instance}, cmd=${cmd}, msg=${JSON.stringify(msg)}, hasCallback=${typeof callback === 'function'})`, 'info', ); - adapter.sendTo( - instance, - cmd, - msg, - timeout && - function (result: any) { - timeout && clearTimeout(timeout); - - sandbox.verbose && - result && - sandbox.log(`sendTo => ${JSON.stringify(result)}`, 'debug'); - - if (typeof callback === 'function') { - try { - callback.call(sandbox, result, options, instance); - } catch (e) { - errorInCallback(e); - } - callback = null; - } - }, - options, - ); + adapter.sendTo(instance, cmd, msg, cbFunc, options); }); }, ); @@ -3858,7 +3947,7 @@ export function sandBox( return new Promise((resolve, reject) => { sandbox.sendTo(_adapter, cmd, msg, options, res => { if (!res || res.error) { - reject(res ? res.error : new Error('Unknown error')); + reject(res ? new Error(res.error) : new Error('Unknown error')); } else { resolve(res); } @@ -3866,7 +3955,7 @@ export function sandBox( }); }, sendToHost: function (host: string, cmd: string, msg?: any, callback?: (result: any) => void): void { - if (!(adapter.config as AdapterConfig).enableSendToHost) { + if (!(adapter.config as JavaScriptAdapterConfig).enableSendToHost) { const error = 'sendToHost is not available. Please enable "Enable SendToHost" option in instance settings'; sandbox.log(error, 'error'); @@ -3887,7 +3976,7 @@ export function sandBox( return new Promise((resolve, reject) => { sandbox.sendToHost(host, cmd, msg, res => { if (!res || res.error) { - reject(res ? res.error : new Error('Unknown error')); + reject(res ? new Error(res.error) : new Error('Unknown error')); } else { resolve(res); } @@ -3897,7 +3986,9 @@ export function sandBox( registerNotification: function (msg: string, isAlert?: boolean): void { const category = !isAlert ? 'scriptMessage' : 'scriptAlert'; - sandbox.verbose && sandbox.log(`registerNotification(msg=${msg}, category=${category})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`registerNotification(msg=${msg}, category=${category})`, 'info'); + } adapter.registerNotification('javascript', category, msg); }, @@ -3906,27 +3997,32 @@ export function sandBox( const int: NodeJS.Timeout = setInterval(() => { try { callback.call(sandbox, ...args); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }, ms); script.intervals.push(int); - sandbox.verbose && sandbox.log(`setInterval(ms=${ms})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`setInterval(ms=${ms})`, 'info'); + } return int; - } else { - sandbox.log(`Invalid callback for setInterval! - ${typeof callback}`, 'error'); - return null; } + sandbox.log(`Invalid callback for setInterval! - ${typeof callback}`, 'error'); + return null; }, clearInterval: function (id: NodeJS.Timeout): void { const pos = script.intervals.indexOf(id); if (pos !== -1) { - sandbox.verbose && sandbox.log('clearInterval() => cleared', 'info'); + if (sandbox.verbose) { + sandbox.log('clearInterval() => cleared', 'info'); + } clearInterval(id); script.intervals.splice(pos, 1); } else { - sandbox.verbose && sandbox.log('clearInterval() => not found', 'warn'); + if (sandbox.verbose) { + sandbox.log('clearInterval() => not found', 'warn'); + } } }, setTimeout: function (callback: (args?: any[]) => void, ms: number, ...args: any[]): NodeJS.Timeout | null { @@ -3940,51 +4036,58 @@ export function sandBox( try { callback.call(sandbox, ...args); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }, ms); - sandbox.verbose && sandbox.log(`setTimeout(ms=${ms})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`setTimeout(ms=${ms})`, 'info'); + } script.timeouts.push(to); return to; - } else { - sandbox.log(`Invalid callback for setTimeout! - ${typeof callback}`, 'error'); - return null; } + sandbox.log(`Invalid callback for setTimeout! - ${typeof callback}`, 'error'); + return null; }, clearTimeout: function (id: NodeJS.Timeout): void { const pos = script.timeouts.indexOf(id); if (pos !== -1) { - sandbox.verbose && sandbox.log('clearTimeout() => cleared', 'info'); + if (sandbox.verbose) { + sandbox.log('clearTimeout() => cleared', 'info'); + } clearTimeout(id); script.timeouts.splice(pos, 1); } else { - sandbox.verbose && sandbox.log('clearTimeout() => not found', 'warn'); + if (sandbox.verbose) { + sandbox.log('clearTimeout() => not found', 'warn'); + } } }, - setImmediate: function (callback: (args: any[]) => void, ...args: any[]): void { + setImmediate: function (callback: (..._args: any[]) => void, ...args: any[]): void { if (typeof callback === 'function') { setImmediate(() => { try { - callback.call(sandbox, ...args); - } catch (e) { - errorInCallback(e); + callback.apply(sandbox, args); + } catch (err: unknown) { + errorInCallback(err as Error); } }); - sandbox.verbose && sandbox.log('setImmediate()', 'info'); + if (sandbox.verbose) { + sandbox.log('setImmediate()', 'info'); + } } else { sandbox.log(`Invalid callback for setImmediate! - ${typeof callback}`, 'error'); } }, - cb: function (callback: (args: any[]) => void): (args: any[]) => void { + cb: function (callback: (..._args: any[]) => void): (...args: any[]) => void { return function (args: any[]) { if (context.scripts[name]?._id === sandbox._id) { if (typeof callback === 'function') { try { - callback.apply(this, args); - } catch (e) { - errorInCallback(e); + callback.apply(sandbox, args); + } catch (err: unknown) { + errorInCallback(err as Error); } } } else { @@ -4002,11 +4105,15 @@ export function sandBox( const pos = consts.astroListLow.indexOf(startTime.toLowerCase()); if (pos !== -1) { const aTime = sandbox.getAstroDate(consts.astroList[pos]); - startTime = aTime.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }); + if (aTime) { + startTime = aTime.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + } else { + startTime = 0; + } } } else if (startTime && isObject(startTime) && (startTime as iobJS.AstroDate).astro) { const aTime = sandbox.getAstroDate( @@ -4014,22 +4121,27 @@ export function sandBox( (startTime as iobJS.AstroDate).date || new Date(), (startTime as iobJS.AstroDate).offset || 0, ); - startTime = aTime.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }); + if (aTime) { + startTime = aTime.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + } else { + startTime = 0; + } } if (endTime && typeof endTime === 'string') { const pos = consts.astroListLow.indexOf(endTime.toLowerCase()); if (pos !== -1) { const aTime = sandbox.getAstroDate(consts.astroList[pos]); - endTime = aTime.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }); + endTime = + aTime?.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }) || 0; } } else if (endTime && isObject(endTime) && (endTime as iobJS.AstroDate).astro) { const aTime = sandbox.getAstroDate( @@ -4037,11 +4149,12 @@ export function sandBox( (endTime as iobJS.AstroDate).date || new Date(), (endTime as iobJS.AstroDate).offset || 0, ); - endTime = aTime.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }); + endTime = + aTime?.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }) || 0; } // --- Convert "time" to number @@ -4050,16 +4163,17 @@ export function sandBox( if (time && typeof time === 'string') { const pos = consts.astroListLow.indexOf(time.toLowerCase()); if (pos !== -1) { - nTime = sandbox.getAstroDate(consts.astroList[pos]).getTime(); + nTime = sandbox.getAstroDate(consts.astroList[pos])?.getTime() || 0; } } else if (time && isObject(time) && (time as iobJS.AstroDate).astro) { - nTime = sandbox - .getAstroDate( - (time as iobJS.AstroDate).astro, - (time as iobJS.AstroDate).date || new Date(), - (time as iobJS.AstroDate).offset || 0, - ) - .getTime(); + nTime = + sandbox + .getAstroDate( + (time as iobJS.AstroDate).astro, + (time as iobJS.AstroDate).date || new Date(), + (time as iobJS.AstroDate).offset || 0, + ) + ?.getTime() || 0; } let daily = true; @@ -4095,8 +4209,6 @@ export function sandBox( } } // --- End of conversion "time" to number - let nStartTime: number; - if (typeof startTime === 'string') { if (!startTime.includes(' ') && !startTime.includes('T')) { const parts = startTime.split(':'); @@ -4118,7 +4230,7 @@ export function sandBox( daily = false; startTime = new Date(startTime as number | Date); } - nStartTime = startTime.getTime(); + const nStartTime = startTime.getTime(); let nEndTime: number | null; if (endTime && typeof endTime === 'string') { @@ -4146,7 +4258,7 @@ export function sandBox( } if (endTime) { - nEndTime = (endTime as Date).getTime(); + nEndTime = endTime.getTime(); } else { nEndTime = null; } @@ -4158,7 +4270,7 @@ export function sandBox( } return nTime >= nStartTime && nTime < nEndTime; } - sandbox.log(`missing or unrecognized endTime expression: ${endTime}`, 'warn'); + sandbox.log(`missing or unrecognized endTime expression: ${JSON.stringify(endTime)}`, 'warn'); return false; } @@ -4169,7 +4281,7 @@ export function sandBox( } return !(nTime >= nStartTime && nTime < nEndTime); } - sandbox.log(`missing or unrecognized endTime expression: ${endTime}`, 'warn'); + sandbox.log(`missing or unrecognized endTime expression: ${JSON.stringify(endTime)}`, 'warn'); return false; } @@ -4191,11 +4303,13 @@ export function sandBox( if (operation === '<>' || operation === '!=') { return nTime !== nStartTime; } - sandbox.log(`Invalid operator: ${operation}`, 'warn'); + sandbox.log(`Invalid operator: ${operation as string}`, 'warn'); return false; }, onStop: function (cb: () => void, timeout?: number): void { - sandbox.verbose && sandbox.log(`onStop(timeout=${timeout})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`onStop(timeout=${timeout})`, 'info'); + } script.onStopCb = cb; script.onStopTimeout = timeout || 1000; @@ -4212,7 +4326,7 @@ export function sandBox( format = objects['system.config'].common.isFloatComma ? '.,' : ',.'; } } - return adapter.formatValue(value, decimals as number, format); + return adapter.formatValue(value, decimals, format); }, formatDate: function ( date: Date | string | number | iobJS.AstroDate, @@ -4234,16 +4348,17 @@ export function sandBox( if (date && typeof date === 'string') { const pos = consts.astroListLow.indexOf(date.toLowerCase()); if (pos !== -1) { - date = sandbox.getAstroDate(consts.astroList[pos]).getTime(); + date = sandbox.getAstroDate(consts.astroList[pos])?.getTime() || 0; } } else if (date && isObject(date) && (date as iobJS.AstroDate).astro) { - date = sandbox - .getAstroDate( - (date as iobJS.AstroDate).astro, - (date as iobJS.AstroDate).date || new Date(), - (date as iobJS.AstroDate).offset || 0, - ) - .getTime(); + date = + sandbox + .getAstroDate( + (date as iobJS.AstroDate).astro, + (date as iobJS.AstroDate).date || new Date(), + (date as iobJS.AstroDate).offset || 0, + ) + ?.getTime() || 0; } if (format.match(/[WНOО]+/)) { @@ -4255,7 +4370,7 @@ export function sandBox( objects['system.config'].common && objects['system.config'].common.language) || 'en'; - if (!consts.dayOfWeeksFull[language]) { + if (!consts.dayOfWeeksFull[language as ioBroker.Languages]) { language = 'en'; } } @@ -4266,25 +4381,25 @@ export function sandBox( return 'Invalid date'; } const d: number = (date as Date).getDay(); - text = text.replace('НН', consts.dayOfWeeksFull[language][d]); + text = text.replace('НН', consts.dayOfWeeksFull[language as ioBroker.Languages][d]); let initialText = text; - text = text.replace('WW', consts.dayOfWeeksFull[language][d]); + text = text.replace('WW', consts.dayOfWeeksFull[language as ioBroker.Languages][d]); if (initialText === text) { - text = text.replace('W', consts.dayOfWeeksShort[language][d]); + text = text.replace('W', consts.dayOfWeeksShort[language as ioBroker.Languages][d]); } - text = text.replace('Н', consts.dayOfWeeksShort[language][d]); - text = text.replace('Н', consts.dayOfWeeksShort[language][d]); + text = text.replace('Н', consts.dayOfWeeksShort[language as ioBroker.Languages][d]); + text = text.replace('Н', consts.dayOfWeeksShort[language as ioBroker.Languages][d]); const m: number = (date as Date).getMonth(); initialText = text; - text = text.replace('OOO', consts.monthFullGen[language][m]); - text = text.replace('ООО', consts.monthFullGen[language][m]); - text = text.replace('OO', consts.monthFull[language][m]); - text = text.replace('ОО', consts.monthFull[language][m]); + text = text.replace('OOO', consts.monthFullGen[language as ioBroker.Languages][m]); + text = text.replace('ООО', consts.monthFullGen[language as ioBroker.Languages][m]); + text = text.replace('OO', consts.monthFull[language as ioBroker.Languages][m]); + text = text.replace('ОО', consts.monthFull[language as ioBroker.Languages][m]); if (initialText === text) { - text = text.replace('O', consts.monthShort[language][m]); + text = text.replace('O', consts.monthShort[language as ioBroker.Languages][m]); } return text; } @@ -4297,7 +4412,9 @@ export function sandBox( let text = format; - sandbox.verbose && sandbox.log(`formatTimeDiff(format=${format}, diff=${diff})`, 'debug'); + if (sandbox.verbose) { + sandbox.log(`formatTimeDiff(format=${format}, diff=${diff})`, 'debug'); + } const second = 1000; const minute = 60 * second; @@ -4312,7 +4429,9 @@ export function sandBox( text = text.replace(/DD|TT|ДД/, days < 10 ? `0${days}` : days.toString()); text = text.replace(/[DTД]/, days.toString()); - sandbox.verbose && sandbox.log(`formatTimeDiff(format=${format}, text=${text}, days=${days})`, 'debug'); + if (sandbox.verbose) { + sandbox.log(`formatTimeDiff(format=${format}, text=${text}, days=${days})`, 'debug'); + } diff -= days * day; } @@ -4352,7 +4471,9 @@ export function sandBox( // diff -= seconds * second; // no milliseconds } - sandbox.verbose && sandbox.log(`formatTimeDiff(format=${format}, text=${text})`, 'debug'); + if (sandbox.verbose) { + sandbox.log(`formatTimeDiff(format=${format}, text=${text})`, 'debug'); + } return neg ? `-${text}` : text; }, @@ -4383,13 +4504,13 @@ export function sandBox( _adapter: string, fileName: string, data: string | Buffer | ((err: Error) => void), - callback?: (err: Error) => void, + callback?: (err?: Error | null) => void, ): void { if (typeof data === 'function' || !data) { - callback = data as (err: Error) => void; + callback = data as (err?: Error | null) => void; data = fileName; fileName = _adapter; - _adapter = null; + _adapter = '0_userdata.0'; } _adapter = _adapter || '0_userdata.0'; @@ -4402,30 +4523,43 @@ export function sandBox( setTimeout(function (): void { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }, 0); } } else { - sandbox.verbose && sandbox.log(`writeFile(adapter=${_adapter}, fileName=${fileName})`, 'info'); - adapter.writeFile(_adapter, fileName, data, callback); + if (sandbox.verbose) { + sandbox.log(`writeFile(adapter=${_adapter}, fileName=${fileName})`, 'info'); + } + if (callback) { + adapter.writeFile(_adapter, fileName, data, callback); + } else { + // @ts-expect-error should be fixed in js-controller + adapter.writeFile(_adapter, fileName, data); + } } }, readFile: function ( _adapter: string, - fileName: string | ((err: Error, data?: Buffer | string, mimeType?: string) => void), - callback?: (err: Error, data?: Buffer | string, mimeType?: string) => void, + fileName: string | ((err: Error | null | undefined, data?: Buffer | string, mimeType?: string) => void), + callback: (err: Error | null | undefined, data?: Buffer | string, mimeType?: string) => void, ): void { if (typeof fileName === 'function') { - callback = fileName as (err: Error, data: Buffer | string) => void; + callback = fileName as ( + err: Error | null | undefined, + data?: Buffer | string, + mimeType?: string, + ) => void; fileName = _adapter; - _adapter = null; + _adapter = '0_userdata.0'; } _adapter = _adapter || '0_userdata.0'; - sandbox.verbose && sandbox.log(`readFile(adapter=${_adapter}, fileName=${fileName})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`readFile(adapter=${_adapter}, fileName=${fileName})`, 'info'); + } - adapter.fileExists(_adapter, fileName, (error: Error | null, result: boolean): void => { + adapter.fileExists(_adapter, fileName, (error: Error | null | undefined, result?: boolean): void => { if (error) { callback(error); } else if (!result) { @@ -4437,13 +4571,13 @@ export function sandBox( }, unlink: function ( _adapter: string, - fileName: string | ((err: Error) => void), - callback?: (err: Error) => void, + fileName: string | ((err?: Error | null) => void), + callback?: (err?: Error | null) => void, ): void { if (typeof fileName === 'function') { callback = fileName; fileName = _adapter; - _adapter = null; + _adapter = '0_userdata.0'; } _adapter = _adapter || '0_userdata.0'; @@ -4456,24 +4590,31 @@ export function sandBox( setTimeout(function (): void { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }, 0); } } else { - sandbox.verbose && sandbox.log(`unlink(adapter=${_adapter}, fileName=${fileName})`, 'info'); - adapter.unlink(_adapter, fileName, callback); + if (sandbox.verbose) { + sandbox.log(`unlink(adapter=${_adapter}, fileName=${fileName})`, 'info'); + } + if (callback) { + adapter.unlink(_adapter, fileName, callback); + } else { + // @ts-expect-error should be fixed in js-controller + adapter.unlink(_adapter, fileName); + } } }, delFile: function ( _adapter: string, - fileName: string | ((err: Error) => void), - callback?: (err: Error) => void, + fileName: string | ((err?: Error | null) => void), + callback?: (err?: Error | null) => void, ): void { return sandbox.unlink(_adapter, fileName as string, callback); }, - rename: function (_adapter: string, oldName: string, newName: string, callback?: (err: Error) => void) { + rename: function (_adapter: string, oldName: string, newName: string, callback?: (err?: Error | null) => void) { _adapter = _adapter || '0_userdata.0'; if (debug) { @@ -4485,22 +4626,27 @@ export function sandBox( setTimeout(function () { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }, 0); } } else { sandbox.verbose && sandbox.log(`rename(adapter=${_adapter}, oldName=${oldName}, newName=${newName})`, 'info'); - adapter.rename(_adapter, oldName, newName, callback); + if (callback) { + adapter.rename(_adapter, oldName, newName, callback); + } else { + // @ts-expect-error should be fixed in js-controller + adapter.rename(_adapter, oldName, newName); + } } }, renameFile: function ( _adapter: string, oldName: string, newName: string, - callback?: (err: Error) => void, + callback?: (err?: Error | null) => void, ): void { return sandbox.rename(_adapter, oldName, newName, callback); }, @@ -4524,7 +4670,7 @@ export function sandBox( instance?: string, ) => void; options = instance as ioBroker.GetHistoryOptions & { id?: string; timeout?: number | string }; - instance = null; + instance = ''; } if (typeof callback !== 'function') { @@ -4553,15 +4699,16 @@ export function sandBox( } } - sandbox.verbose && - sandbox.log(`getHistory(instance=${instance}, options=${JSON.stringify(options)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`getHistory(instance=${instance as string}, options=${JSON.stringify(options)})`, 'info'); + } if (!instance) { sandbox.log('No default history instance found!', 'error'); try { - callback.call(sandbox, 'No default history instance found!'); - } catch (e) { - errorInCallback(e); + callback.call(sandbox, new Error('No default history instance found!')); + } catch (err: unknown) { + errorInCallback(err as Error); } return; } @@ -4569,26 +4716,35 @@ export function sandBox( instance = (instance as string).substring('system.adapter.'.length); } - if (!objects[`system.adapter.${instance}`]) { - sandbox.log(`Instance "${instance}" not found!`, 'error'); + if (!objects[`system.adapter.${instance as string}`]) { + sandbox.log(`Instance "${instance as string}" not found!`, 'error'); try { - callback.call(sandbox, `Instance "${instance}" not found!`); - } catch (e) { - errorInCallback(e); + callback.call(sandbox, new Error(`Instance "${instance as string}" not found!`)); + } catch (err: unknown) { + errorInCallback(err as Error); } return; } - const timeout = setTimeout(() => { - sandbox.verbose && sandbox.log('getHistory => timeout', 'debug'); + let _timeout: NodeJS.Timeout | null = setTimeout(() => { + _timeout = null; + if (sandbox.verbose) { + sandbox.log('getHistory => timeout', 'debug'); + } if (typeof callback === 'function') { try { - callback.call(sandbox, 'Timeout', null, options, instance); - } catch (e) { - errorInCallback(e); + callback.call( + sandbox, + new Error('Timeout'), + null, + options as ioBroker.GetHistoryOptions & { id: string; timeout?: number | string }, + instance as string, + ); + } catch (err: unknown) { + errorInCallback(err as Error); } - callback = null; + callback = undefined; } }, timeoutMs); @@ -4600,26 +4756,37 @@ export function sandBox( options, }, (res: any): void => { - timeout && clearTimeout(timeout); + if (_timeout) { + clearTimeout(_timeout); + _timeout = null; + } const result: { - error?: Error; + error?: string; result?: ioBroker.GetHistoryResult; step?: number; sessionId?: string; } = res; - sandbox.verbose && result?.error && sandbox.log(`getHistory => ${result.error}`, 'error'); - sandbox.verbose && - result?.result && + if (sandbox.verbose && result?.error) { + sandbox.log(`getHistory => ${result.error}`, 'error'); + } + if (sandbox.verbose && result?.result) { sandbox.log(`getHistory => ${result.result.length} items`, 'debug'); + } if (typeof callback === 'function') { try { - callback.call(sandbox, result.error, result.result, options, instance); - } catch (e) { - errorInCallback(e); + callback.call( + sandbox, + result.error ? new Error(result.error) : null, + result.result, + options as ioBroker.GetHistoryOptions & { id: string; timeout?: number | string }, + instance as string, + ); + } catch (err: unknown) { + errorInCallback(err as Error); } - callback = null; + callback = undefined; } }, ); @@ -4650,7 +4817,6 @@ export function sandBox( { common: { enabled: true } }, err => typeof callback === 'function' && callback(err), ); - scriptName = null; }); return true; } @@ -4662,27 +4828,28 @@ export function sandBox( return true; }, runScriptAsync: function (scriptName: string): Promise { + let done = false; return new Promise((resolve, reject) => { const result = sandbox.runScript(scriptName, err => { if (err) { reject(err); - reject = null; + done = true; } else { resolve(); } }); - if (result === false && reject) { - reject(`Script ${scriptName} was not found!`); + if (result === false && !done) { + reject(new Error(`Script ${scriptName} was not found!`)); } }); }, startScript: function ( scriptName: string, - ignoreIfStarted?: boolean | ((err: Error | null, started: boolean) => void), - callback?: (err: Error | null, started: boolean) => void, + ignoreIfStarted?: boolean | ((err: Error | null | undefined, started: boolean) => void), + callback?: (err: Error | null | undefined, started: boolean) => void, ): boolean { if (typeof ignoreIfStarted === 'function') { - callback = ignoreIfStarted as (err: Error | null, started: boolean) => void; + callback = ignoreIfStarted as (err: Error | null | undefined, started: boolean) => void; ignoreIfStarted = false; } scriptName = scriptName || name; @@ -4711,7 +4878,6 @@ export function sandBox( { common: { enabled: true } }, err => typeof callback === 'function' && callback(err, true), ); - scriptName = null; }); } else if (typeof callback === 'function') { callback(null, false); @@ -4727,8 +4893,8 @@ export function sandBox( return new Promise((resolve, reject) => { const result = sandbox.startScript( scriptName, - ignoreIfStarted, - (err: Error | null, started: boolean): void => { + !!ignoreIfStarted, + (err: Error | null | undefined, started: boolean): void => { if (err) { reject(err); } else { @@ -4737,11 +4903,14 @@ export function sandBox( }, ); if (result === false) { - reject(`Script ${scriptName} was not found!`); + reject(new Error(`Script ${scriptName} was not found!`)); } }); }, - stopScript: function (scriptName: string, callback?: (err: Error | null, stopped: boolean) => void): boolean { + stopScript: function ( + scriptName: string, + callback?: (err: Error | null | undefined, stopped: boolean) => void, + ): boolean { scriptName = scriptName || name; if (!scriptName.match(/^script\.js\./)) { @@ -4758,14 +4927,17 @@ export function sandBox( `stopScript(scriptName=${scriptName}) - ${words._('was not executed, while debug mode is active')}`, 'warn', ); - typeof callback === 'function' && callback(null, false); + if (typeof callback === 'function') { + callback(null, false); + } return true; } if (objects[scriptName].common.enabled) { objects[scriptName].common.enabled = false; adapter.extendForeignObject(scriptName, { common: { enabled: false } }, err => { - typeof callback === 'function' && callback(err, true); - scriptName = null; + if (typeof callback === 'function') { + callback(err, true); + } }); } else if (typeof callback === 'function') { callback(null, false); @@ -4774,15 +4946,18 @@ export function sandBox( }, stopScriptAsync: function (scriptName: string): Promise { return new Promise((resolve, reject) => { - const result = sandbox.stopScript(scriptName, (err: Error | null, stopped: boolean): void => { - if (err) { - reject(err); - } else { - resolve(stopped); - } - }); + const result = sandbox.stopScript( + scriptName, + (err: Error | null | undefined, stopped: boolean): void => { + if (err) { + reject(err); + } else { + resolve(stopped); + } + }, + ); if (result === false) { - reject(`Script ${scriptName} was not found!`); + reject(new Error(`Script ${scriptName} was not found!`)); } }); }, @@ -4806,7 +4981,9 @@ export function sandBox( if (instanceObj?.type === 'instance' && !instanceObj.common.enabled) { await adapter.extendForeignObjectAsync(objInstanceId, { common: { enabled: true } }); - sandbox.verbose && sandbox.log(`startInstanceAsync (instanceName=${instanceName})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`startInstanceAsync (instanceName=${instanceName})`, 'info'); + } return true; } @@ -4827,7 +5004,9 @@ export function sandBox( if (instanceObj?.type === 'instance' && instanceObj.common.enabled) { await adapter.extendForeignObjectAsync(objInstanceId, {}); - sandbox.verbose && sandbox.log(`restartInstanceAsync (instanceName=${instanceName})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`restartInstanceAsync (instanceName=${instanceName})`, 'info'); + } return true; } @@ -4848,7 +5027,9 @@ export function sandBox( if (instanceObj?.type === 'instance' && instanceObj.common.enabled) { await adapter.extendForeignObjectAsync(objInstanceId, { common: { enabled: false } }); - sandbox.verbose && sandbox.log(`stopInstanceAsync (instanceName=${instanceName})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`stopInstanceAsync (instanceName=${instanceName})`, 'info'); + } return true; } @@ -4859,6 +5040,7 @@ export function sandBox( return false; }, + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents toInt: function (val: boolean | string | number | 'true' | 'false'): number { if (val === true || val === 'true') { val = 1; @@ -4869,6 +5051,7 @@ export function sandBox( val = parseInt(val as unknown as string) || 0; return val; }, + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents toFloat: function (val: boolean | string | number | 'true' | 'false'): number { if (val === true || val === 'true') { val = 1; @@ -4879,6 +5062,7 @@ export function sandBox( val = parseFloat(val as unknown as string) || 0; return val; }, + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents toBoolean: function (val: boolean | string | number | 'true' | 'false'): boolean { if (val === '1' || val === 'true') { val = true; @@ -4895,28 +5079,28 @@ export function sandBox( if (typeof obj === 'string') { try { obj = JSON.parse(obj); - } catch (e) { + } catch (err: unknown) { adapter.setState(`scriptProblem.${name.substring('script.js.'.length)}`, { val: true, ack: true, c: 'getAttr', }); - sandbox.log(`Cannot parse "${obj.substring(0, 30)}": ${e}`, 'error'); + sandbox.log(`Cannot parse "${obj.substring(0, 30)}": ${err as Error}`, 'error'); return null; } } - const attr = path.shift(); + const attr: string = path.shift() || ''; try { obj = obj[attr]; - } catch (e) { + } catch (err: unknown) { adapter.setState(`scriptProblem.${name.substring('script.js.'.length)}`, { val: true, ack: true, c: 'getAttr', }); - sandbox.log(`Cannot get ${attr} of "${JSON.stringify(obj)}": ${e}`, 'error'); + sandbox.log(`Cannot get ${attr} of "${JSON.stringify(obj)}": ${err as Error}`, 'error'); return null; } @@ -4953,18 +5137,43 @@ export function sandBox( timeout = setTimeout(() => { timeout = null; - sandbox.verbose && sandbox.log(`messageTo => timeout: ${timeoutDuration}`, 'debug'); + if (sandbox.verbose) { + sandbox.log(`messageTo => timeout: ${timeoutDuration}`, 'debug'); + } if (typeof callback === 'function') { try { callback.call(sandbox, { error: 'timeout' }, options, target.instance); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } - callback = null; + callback = undefined; } }, timeoutDuration); } + let cbFunc: undefined | ((result: any) => void); + if (timeout) { + cbFunc = function (res: any) { + timeout && clearTimeout(timeout); + const result: { result?: any; error?: string | null } = res; + + if (sandbox.verbose && result?.result) { + sandbox.log(`messageTo => ${JSON.stringify(result)}`, 'debug'); + } + if (sandbox.verbose && result?.error) { + sandbox.log(`messageTo => ${result.error}`, 'error'); + } + + if (typeof callback === 'function') { + try { + callback.call(sandbox, result, options, target.instance); + } catch (err: unknown) { + errorInCallback(err as Error); + } + callback = undefined; + } + }; + } if (target.instance || target.instance === 0) { if ( @@ -4981,24 +5190,7 @@ export function sandBox( target.instance, 'jsMessageBus', { message: target.message, script: target.script, data }, - timeout && - function (res: any) { - timeout && clearTimeout(timeout); - const result: { result?: any; error?: Error | null } = res; - - sandbox.verbose && - result?.result && - sandbox.log(`messageTo => ${JSON.stringify(result)}`, 'debug'); - - if (typeof callback === 'function') { - try { - callback.call(sandbox, result, options, target.instance); - } catch (e) { - errorInCallback(e); - } - callback = null; - } - }, + cbFunc, ); } else { // Send it to all instances @@ -5007,9 +5199,9 @@ export function sandBox( 'instance', { startkey: 'system.adapter.javascript.', endkey: 'system.adapter.javascript.\u9999' }, options, - (err: Error | null, res): void => { + (err: Error | null | undefined, res): void => { if (err || !res) { - sandbox.log(`messageTo failed: ${err.message}`, 'error'); + sandbox.log(`messageTo failed: ${err?.message}`, 'error'); return; } const len = 'system.adapter.'.length; @@ -5020,23 +5212,7 @@ export function sandBox( instance, 'jsMessageBus', { message: target.message, script: target.script, data }, - timeout && - function (result: any): void { - timeout && clearTimeout(timeout); - - if (typeof callback === 'function') { - sandbox.verbose && - result?.result && - sandbox.log(`messageTo result => ${JSON.stringify(result)}`, 'info'); - - try { - callback.call(sandbox, result, options, target.instance); - } catch (e) { - errorInCallback(e); - } - callback = null; - } - }, + cbFunc, ); }); }, @@ -5050,10 +5226,12 @@ export function sandBox( ): Promise { return new Promise((resolve, reject) => { sandbox.messageTo(target, data, options, (res: any): void => { - const result: { error?: Error } = res; - sandbox.verbose && sandbox.log(`messageTo result => ${JSON.stringify(res)}`, 'debug'); + const result: { error?: string } = res; + if (sandbox.verbose) { + sandbox.log(`messageTo result => ${JSON.stringify(res)}`, 'debug'); + } if (!res || result.error) { - reject(result ? result.error : new Error('Unknown error')); + reject(result ? new Error(result.error) : new Error('Unknown error')); } else { resolve(result); } @@ -5079,7 +5257,8 @@ export function sandBox( sandbox.__engine.__subscriptionsMessage += 1; if ( - sandbox.__engine.__subscriptionsMessage % (adapter.config as AdapterConfig).maxTriggersPerScript === + sandbox.__engine.__subscriptionsMessage % + (adapter.config as JavaScriptAdapterConfig).maxTriggersPerScript === 0 ) { sandbox.log( @@ -5152,13 +5331,13 @@ export function sandBox( onObject: function ( pattern: string | string[], callback: (id: string, obj?: ioBroker.Object | null) => void, - ): SubscribeObject | SubscribeObject[] { + ): SubscribeObject | SubscribeObject[] | null { return sandbox.subscribeObject(pattern, callback); }, subscribeObject: function ( pattern: string | string[], callback: (id: string, obj?: ioBroker.Object | null) => void, - ): SubscribeObject | SubscribeObject[] { + ): SubscribeObject | SubscribeObject[] | null { if (Array.isArray(pattern)) { const result: { name: string; @@ -5179,7 +5358,11 @@ export function sandBox( sandbox.__engine.__subscriptionsObject += 1; - if (sandbox.__engine.__subscriptionsObject % (adapter.config as AdapterConfig).maxTriggersPerScript === 0) { + if ( + sandbox.__engine.__subscriptionsObject % + (adapter.config as JavaScriptAdapterConfig).maxTriggersPerScript === + 0 + ) { sandbox.log( `More than ${sandbox.__engine.__subscriptionsObject} object subscriptions registered. Check your script!`, 'warn', @@ -5189,16 +5372,18 @@ export function sandBox( // source is set by regexp if defined as /regexp/ if (!pattern || typeof pattern !== 'string') { sandbox.log('Error by subscribeObject: pattern can be only string or array of strings.', 'error'); - return; + return null; } if (typeof callback !== 'function') { sandbox.log('Error by subscribeObject: callback is not a function', 'error'); - return; + return null; } - const subs: SubscribeObject = { pattern: pattern as string, callback, name }; - sandbox.verbose && sandbox.log(`subscribeObject: ${JSON.stringify(subs)}`, 'info'); + const subs: SubscribeObject = { pattern, callback, name }; + if (sandbox.verbose) { + sandbox.log(`subscribeObject: ${JSON.stringify(subs)}`, 'info'); + } adapter.subscribeForeignObjects(pattern); @@ -5215,7 +5400,9 @@ export function sandBox( return result; } - sandbox.verbose && sandbox.log(`adapterUnsubscribeObject(id=${JSON.stringify(subObject)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`adapterUnsubscribeObject(id=${JSON.stringify(subObject)})`, 'info'); + } for (let i = context.subscriptionsObject.length - 1; i >= 0; i--) { if (context.subscriptionsObject[i] === subObject) { @@ -5229,10 +5416,10 @@ export function sandBox( for (let i = context.subscriptionsObject.length - 1; i >= 0; i--) { if ( context.subscriptionsObject[i].name && - context.subscriptionsObject[i].pattern === (subObject as SubscribeObject).pattern + context.subscriptionsObject[i].pattern === subObject.pattern ) { deleted++; - adapter.unsubscribeForeignObjects((subObject as SubscribeObject).pattern); + adapter.unsubscribeForeignObjects(subObject.pattern); context.subscriptionsObject.splice(i, 1); sandbox.__engine.__subscriptionsObject--; } @@ -5251,11 +5438,11 @@ export function sandBox( }, }; - if ((adapter.config as AdapterConfig).enableSetObject) { + if ((adapter.config as JavaScriptAdapterConfig).enableSetObject) { sandbox.setObject = function ( id: string, obj: ioBroker.Object, - callback?: (err?: Error | null | undefined, res?: { id: string }) => void, + callback?: (err?: Error | null, res?: { id: string }) => void, ): void { if (id && typeof id === 'string' && id.startsWith('system.adapter.')) { sandbox.log( @@ -5272,13 +5459,15 @@ export function sandBox( setImmediate(function () { try { callback.call(sandbox, null, { id }); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }); } } else { - sandbox.verbose && sandbox.log(`setObject(id=${id}, obj=${JSON.stringify(obj)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`setObject(id=${id}, obj=${JSON.stringify(obj)})`, 'info'); + } adapter.setForeignObject(id, obj, (err, res) => { if (!err) { // Update meta object data @@ -5287,8 +5476,8 @@ export function sandBox( if (typeof callback === 'function') { try { callback.call(sandbox, err, res); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } } }); @@ -5308,13 +5497,15 @@ export function sandBox( setTimeout(function () { try { callback.call(sandbox, null, { id }); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }, 0); } } else { - sandbox.verbose && sandbox.log(`extendObject(id=${id}, obj=${JSON.stringify(obj)})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`extendObject(id=${id}, obj=${JSON.stringify(obj)})`, 'info'); + } adapter.extendForeignObject(id, JSON.parse(JSON.stringify(obj)), callback); } }; @@ -5332,13 +5523,15 @@ export function sandBox( setTimeout(function () { try { callback.call(sandbox); - } catch (e) { - errorInCallback(e); + } catch (err: unknown) { + errorInCallback(err as Error); } }, 0); } } else { - sandbox.verbose && sandbox.log(`deleteObject(id=${id})`, 'info'); + if (sandbox.verbose) { + sandbox.log(`deleteObject(id=${id})`, 'info'); + } adapter.delForeignObject(id, { recursive: isRecursive }, callback); } }; diff --git a/src/lib/scheduler.ts b/src/lib/scheduler.ts index b730c05cb..fed6e910a 100644 --- a/src/lib/scheduler.ts +++ b/src/lib/scheduler.ts @@ -26,7 +26,7 @@ // } // }; -import { +import type { GetMoonIlluminationResult, GetMoonPositionResult, GetMoonTimes, @@ -167,7 +167,7 @@ export class Scheduler { private timer: NodeJS.Timeout | null = null; private todaysAstroTimes: Record; private yesterdaysAstroTimes: Record; - private astroList: AstroEventName[] | undefined; + private readonly astroList: AstroEventName[] | undefined; private astroListLow: AstroEventNameLow[] | undefined; constructor( @@ -251,7 +251,7 @@ export class Scheduler { ]; } - _getId() { + _getId(): string { return `${Math.round(Math.random() * 1000000)}.${this.Date.now()}`; } @@ -328,19 +328,19 @@ export class Scheduler { return months <= 0 ? 0 : months; } - checkSchedule(context: SchedulerContext, schedule: SchedulerRuleParsed) { + checkSchedule(context: SchedulerContext, schedule: SchedulerRuleParsed): boolean { if (schedule.valid) { if ( schedule.valid.from && !this.isPast(context, schedule.valid.from) && !this.isToday(context, schedule.valid.from) ) { - return; + return false; } // "to" this.Date is in the past => delete it from a list if (schedule.valid.to && this.isPast(context, schedule.valid.to)) { delete this.list[schedule.id]; - return; + return false; } } if (schedule.period) { @@ -348,31 +348,31 @@ export class Scheduler { if (this.isPast(context, schedule.period.once)) { delete this.list[schedule.id]; } - return; + return false; } else if (schedule.period.days) { if (schedule.period.dows && !schedule.period.dows.includes(context.dow)) { - return; + return false; } else if (schedule.period.days > 1) { // @ts-expect-error period of days cannot be without valid fromDate const diff = Math.round((context.now - schedule.valid.fromDate) / (60000 * 24) + 0.5); if (diff % schedule.period.days) { - return; + return false; } } } else if (schedule.period.weeks) { if (schedule.period.dows && !schedule.period.dows.includes(context.dow)) { - return; + return false; } if (schedule.period.weeks > 1) { // @ts-expect-error period of weeks cannot be without valid fromDate const diff = Math.round((context.now - schedule.valid.fromDate) / (60000 * 24 * 7) + 0.5); if (diff % schedule.period.weeks) { - return; + return false; } } } else if (schedule.period.months) { if (Array.isArray(schedule.period.months) && !schedule.period.months.includes(context.M)) { - return; + return false; } if ( schedule.period.fromDate && @@ -381,18 +381,18 @@ export class Scheduler { ) { const diff = this.monthDiff(schedule.period.fromDate, new this.Date(context.now)); if (diff % schedule.period.months) { - return; + return false; } } if (schedule.period.dates && !schedule.period.dates.includes(context.d)) { - return; + return false; } } else if (schedule.period.years) { if (schedule.period.yearMonth !== undefined && schedule.period.yearMonth !== context.M) { - return; + return false; } if (schedule.period.yearDate && schedule.period.yearDate !== context.d) { - return; + return false; } if ( schedule.period.fromDate && @@ -401,7 +401,7 @@ export class Scheduler { ) { const diff = Math.floor(this.monthDiff(schedule.period.fromDate, new this.Date(context.now)) / 12); if (diff % schedule.period.years) { - return; + return false; } } } @@ -426,7 +426,7 @@ export class Scheduler { start = startDate.getHours() * 60 + startDate.getMinutes(); } else { this.log.error(`unknown astro event "${schedule.time.start}"`); - return; + return false; } } else { start = schedule.time.start; @@ -442,7 +442,7 @@ export class Scheduler { end = endDate.getHours() * 60 + endDate.getMinutes(); } else { this.log.error(`unknown astro event "${schedule.time.end}"`); - return; + return false; } } else { end = schedule.time.end; @@ -453,19 +453,19 @@ export class Scheduler { if (schedule.time.exactTime) { if (context.minutesOfDay !== start) { - return; + return false; } } else { if (start >= context.minutesOfDay || (end && end < context.minutesOfDay)) { - return; + return false; } if (schedule.time.mode === 60) { if (schedule.time.interval > 1 && (context.minutesOfDay - start) % schedule.time.interval) { - return; + return false; } } else if (schedule.time.mode === 3600) { if ((context.minutesOfDay - start) % (schedule.time.interval * 60)) { - return; + return false; } } } @@ -513,7 +513,7 @@ export class Scheduler { if (typeof schedule === 'string') { try { oSchedule = JSON.parse(schedule); - } catch (e) { + } catch { this.log.error(`Cannot parse schedule: ${schedule}`); return null; } @@ -623,8 +623,8 @@ export class Scheduler { if (oSchedule.period.dows) { try { sch.period.dows = JSON.parse(oSchedule.period.dows); - } catch (e) { - this.log.error(`Cannot parse day of weeks: ${sch.period.dows}`); + } catch { + this.log.error(`Cannot parse day of weeks: ${JSON.stringify(sch.period.dows)}`); return null; } if (!Array.isArray(sch.period.dows)) { @@ -637,8 +637,8 @@ export class Scheduler { // can be number or array-string try { sch.period.months = JSON.parse(oSchedule.period.months) as number[]; - } catch (e) { - this.log.error(`Cannot parse day of months: ${sch.period.months}`); + } catch { + this.log.error(`Cannot parse day of months: ${JSON.stringify(sch.period.months)}`); return null; } sch.period.months = sch.period.months.map(m => m - 1); @@ -647,8 +647,8 @@ export class Scheduler { if (oSchedule.period.dates) { try { sch.period.dates = JSON.parse(oSchedule.period.dates) as number[]; - } catch (e) { - this.log.error(`Cannot parse day of dates: ${sch.period.dates}`); + } catch { + this.log.error(`Cannot parse day of dates: ${JSON.stringify(sch.period.dates)}`); return null; } } @@ -660,7 +660,7 @@ export class Scheduler { return id; } - remove(id: string | ScheduleName) { + remove(id: string | ScheduleName): boolean { if ( typeof id === 'object' && id.type === 'schedule' && diff --git a/src/lib/tools.ts b/src/lib/tools.ts index 97064c2cb..c46aac594 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -6,7 +6,8 @@ import type { AxiosRequestConfig, ResponseType } from 'axios'; /** * Tests whether the given variable is a real object and not an Array - * @param {any} it The variable to test + * + * @param it The variable to test */ export function isObject(it: any): boolean { // This is necessary because: @@ -18,8 +19,8 @@ export function isObject(it: any): boolean { /** * Tests whether the given variable is really an Array - * @param {any} it The variable to test - * @returns {it is any[]} + * + * @param it The variable to test */ export function isArray(it: any): boolean { return Array.isArray(it); @@ -27,8 +28,9 @@ export function isArray(it: any): boolean { /** * Finds all matches of a regular expression and returns the matched groups - * @param {RegExp} regex The regular expression to match against the string - * @param {string} string The string to test + * + * @param regex The regular expression to match against the string + * @param string The string to test */ export function matchAll(regex: RegExp, string: string): string[][] { const ret: string[][] = []; @@ -62,8 +64,8 @@ export function enumFilesRecursiveSync(rootDir: string, predicate: (filename: st ret.push(fullPath); } } - } catch (err) { - console.error(`Cannot read directory: "${rootDir}": ${err}`); + } catch (err: unknown) { + console.error(`Cannot read directory: "${rootDir}": ${err as Error}`); } return ret; @@ -71,30 +73,31 @@ export function enumFilesRecursiveSync(rootDir: string, predicate: (filename: st /** * Promisifies a callback-style function with parameters (err, result) - * @param {(...args: any[]) => any} fn The callback-style function to promisify - * @param {*} [context] The value of `this` in the function + * + * @param fn The callback-style function to promisify + * @param context The value of `this` in the function */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function promisify(fn: Function, context?: any): (...args: any[]) => Promise { return function (...args) { - // @ts-ignore We want this behavior + // @ts-expect-error We want this behavior context = context || this; return new Promise((resolve, reject) => { try { fn.apply(context, [ ...args, - (error, result) => { + (error: Error | string | null, result: null) => { if (error) { if (typeof error === 'string') { return reject(new Error(error)); } return reject(error); - } else { - return resolve(result); } + return resolve(result); }, ]); - } catch (error) { - reject(error); + } catch (error: unknown) { + reject(error as Error); } }); }; @@ -102,17 +105,19 @@ export function promisify(fn: Function, context?: any): (...args: any[]) => Prom /** * Promisifies a callback-style function without an error parameter - * @param {(...args: any[]) => any} fn The callback-style function to promisify - * @param {*} [context] The value of `this` in the function + * + * @param fn The callback-style function to promisify + * @param context The value of `this` in the function */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function promisifyNoError(fn: Function, context: any): (...args: any[]) => Promise { return function (...args) { - // @ts-ignore We want this behavior + // @ts-expect-error We want this behavior context = context || this; return new Promise(resolve => { try { fn.apply(context, [...args, result => resolve(result)]); - } catch (error) { + } catch { resolve(null); // what to do in this case?? } }); @@ -121,8 +126,8 @@ export function promisifyNoError(fn: Function, context: any): (...args: any[]) = /** * Creates an MD5 hash of a script source which can be used to check whether the source of a compiled language changed - * @param {string} source The source code to hash - * @returns {string} + * + * @param source The source code to hash */ export function hashSource(source: string | Buffer): string { return createHash('md5').update(source).digest('hex'); @@ -160,7 +165,7 @@ export function getHttpRequestConfig( password: options.basicAuth.password, }; } else if (options.bearerAuth) { - options.headers['Authorization'] = `Bearer ${options.bearerAuth}`; + options.headers.Authorization = `Bearer ${options.bearerAuth}`; } else { const uri = new URL(url); if (uri.username && uri.password) { diff --git a/src/lib/typescriptTools.ts b/src/lib/typescriptTools.ts index 18ec579e3..1f4fc1a08 100644 --- a/src/lib/typescriptTools.ts +++ b/src/lib/typescriptTools.ts @@ -24,7 +24,7 @@ import { SyntaxKind, type Modifier, NodeFlags, - ModuleDeclaration, + type ModuleDeclaration, visitNode, visitNodes, isSourceFile, @@ -32,12 +32,8 @@ import { type NodeArray, type TransformerFactory, type SourceFile, - TransformationContext, - Transformer, - ModifierLike, - NamedExportBindings, - Expression, - ImportAttributes, ExportDeclaration, + type TransformationContext, + type Transformer, } from 'typescript'; import { matchAll } from './tools'; @@ -47,7 +43,7 @@ import { matchAll } from './tools'; * @param targetLib The lib to target (e.g., es2017) */ export function resolveTypescriptLibs( - targetLib: 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'esnext', + targetLib: 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022' | 'esnext', ): Record { const typescriptLibRoot = dirname(require.resolve(`typescript/lib/lib.d.ts`)); const ret: Record = {}; @@ -92,7 +88,7 @@ function normalizeDTSImport(filename: string): string { * * @param pkg The package whose typings we're interested in * @param adapterScopedPackageName the package name on the system - * @param [wrapInDeclareModule=false] Whether the root file should be wrapped in `declare module "" { ... }` + * @param wrapInDeclareModule Whether the root file should be wrapped in `declare module "" { ... }` * @returns The found declarations or undefined if none were found */ export function resolveTypings( @@ -140,7 +136,7 @@ export function resolveTypings( } const packageRoot: string = dirname(packageJsonPath); - const normalizeImportPath = (filename: string) => + const normalizeImportPath = (filename: string): string => normalize( `node_modules/${pkgIncludesTypings ? adapterScopedPackageName : `@types/${pkg}`}/${relative(packageRoot, filename)}`, ).replace(/\\/g, '/'); @@ -174,9 +170,9 @@ export function resolveTypings( let fileContent: string; try { fileContent = readFileSync(filename, 'utf8'); - } catch (e) { + } catch (e: any) { // The typings are malformed - console.error(`Failed to load definitions for ${pkg}: ${e}`); + console.error(`Failed to load definitions for ${pkg}: ${e.toString()}`); // Since we cannot use them, return undefined return undefined; } @@ -211,25 +207,25 @@ export function resolveTypings( function mustBeHoisted(s: Statement & { modifiers?: Modifier[] }, isGlobal?: boolean): boolean { return !!( // Import/export statements must be moved to the top - isImportDeclaration(s) || - isImportEqualsDeclaration(s) || - isExportDeclaration(s) || - isExportAssignment(s) || - // as well as many declarations - isTypeAliasDeclaration(s) || - isInterfaceDeclaration(s) || - isModuleDeclaration(s) || - isEnumDeclaration(s) || - (isGlobal && - // in global scripts we don't wrap classes and functions, so they can be accessed from non-global scripts - (isClassDeclaration(s) || isFunctionDeclaration(s))) || - // and declare ... / export ... statements - (s.modifiers?.some(s => - s.kind === SyntaxKind.DeclareKeyword || s.kind === SyntaxKind.ExportKeyword)) + ( + isImportDeclaration(s) || + isImportEqualsDeclaration(s) || + isExportDeclaration(s) || + isExportAssignment(s) || + // as well as many declarations + isTypeAliasDeclaration(s) || + isInterfaceDeclaration(s) || + isModuleDeclaration(s) || + isEnumDeclaration(s) || + (isGlobal && + // in global scripts we don't wrap classes and functions, so they can be accessed from non-global scripts + (isClassDeclaration(s) || isFunctionDeclaration(s))) || + // and declare ... / export ... statements + s.modifiers?.some(s => s.kind === SyntaxKind.DeclareKeyword || s.kind === SyntaxKind.ExportKeyword) + ) ); } -/** @param {import("typescript").Statement} s */ function canBeExported(s: Statement): boolean { return ( // const, let, var @@ -243,7 +239,7 @@ function canBeExported(s: Statement): boolean { ); } -function addExportModifier(s: Statement & { modifiers?: Modifier[] }) { +function addExportModifier(s: Statement & { modifiers?: Modifier[] }): Statement { let modifiers: Modifier[] | undefined; // Add export modifiers if (!s.modifiers) { @@ -261,14 +257,7 @@ function addExportModifier(s: Statement & { modifiers?: Modifier[] }) { return factory.updateTypeAliasDeclaration(s, modifiers, s.name, s.typeParameters, s.type); } if (isInterfaceDeclaration(s)) { - return factory.updateInterfaceDeclaration( - s, - modifiers, - s.name, - s.typeParameters, - s.heritageClauses, - s.members, - ); + return factory.updateInterfaceDeclaration(s, modifiers, s.name, s.typeParameters, s.heritageClauses, s.members); } if (isEnumDeclaration(s)) { return factory.updateEnumDeclaration(s, modifiers, s.name, s.members); @@ -291,7 +280,7 @@ function addExportModifier(s: Statement & { modifiers?: Modifier[] }) { return s; } -function removeDeclareModifier(s: Statement & { modifiers?: Modifier[] }): Statement { +function removeDeclareModifier(s: Statement & { modifiers?: Modifier[] }): Statement { let modifiers: Modifier[] | undefined; // Remove declare modifiers if (s.modifiers) { @@ -307,14 +296,7 @@ function removeDeclareModifier(s: Statement & { modifiers?: Modifier[] }): State return factory.updateTypeAliasDeclaration(s, modifiers, s.name, s.typeParameters, s.type); } if (isInterfaceDeclaration(s)) { - return factory.updateInterfaceDeclaration( - s, - modifiers, - s.name, - s.typeParameters, - s.heritageClauses, - s.members, - ); + return factory.updateInterfaceDeclaration(s, modifiers, s.name, s.typeParameters, s.heritageClauses, s.members); } if (isEnumDeclaration(s)) { return factory.updateEnumDeclaration(s, modifiers, s.name, s.members); @@ -407,7 +389,6 @@ const NodeJSGlobals = [ 'v8debug', ]; -/** @param {import("typescript").Statement} s */ function isGlobalAugmentation(s: Statement): boolean { return !!( (isInterfaceDeclaration(s) || isClassDeclaration(s) || isFunctionDeclaration(s)) && @@ -432,7 +413,6 @@ function wrapInDeclareGlobal(statements: Statement[]): ModuleDeclaration { * @param isGlobal Whether the transformed script is a global script or not */ export function transformScriptBeforeCompilation(source: string, isGlobal?: boolean): string { - // eslint-disable-next-line no-unused-vars const transformer: TransformerFactory = (_context: TransformationContext): Transformer => { return (sourceFile: SourceFile) => visitNode(sourceFile, (node: SourceFile): any => { @@ -460,7 +440,7 @@ export function transformScriptBeforeCompilation(source: string, isGlobal?: bool hoistedStatements, // @ts-expect-error s is definitely a Statement s => (canBeExported(s) ? addExportModifier(s) : s), - ) as NodeArray + ) as NodeArray; // 3. We need to transform the generated declarations to use `declare global` (this will happen in transformGlobalDeclarations) } const needsEmptyExport = @@ -504,9 +484,9 @@ export function transformScriptBeforeCompilation(source: string, isGlobal?: bool ...(needsEmptyExport ? [ // Put an empty export {}; at the bottom to force TypeScript to treat the script as a module - factory.createExportDeclaration( + factory.createExportDeclaration( undefined, // ModifierLike[] | undefined - false, // isTypeOnly + false, // isTypeOnly factory.createNamedExports([]), // NamedExportBindings | undefined undefined, // moduleSpecifier undefined, // attributes @@ -514,9 +494,8 @@ export function transformScriptBeforeCompilation(source: string, isGlobal?: bool ] : []), ]); - } else { - return node; } + return node; }); }; @@ -528,6 +507,7 @@ export function transformScriptBeforeCompilation(source: string, isGlobal?: bool /** * Takes the global declarations for a TypeScript and wraps export statements in `declare global` + * * @param decl The untransformed global declarations */ export function transformGlobalDeclarations(decl: string): string { @@ -543,9 +523,7 @@ export function transformGlobalDeclarations(decl: string): string { const otherStatements = node.statements.filter(s => !exportStatements.includes(s)); const hasExportStatements = exportStatements.length > 0; - const hasImport = otherStatements.some( - s => isImportDeclaration(s) || isImportEqualsDeclaration(s), - ); + const hasImport = otherStatements.some(s => isImportDeclaration(s) || isImportEqualsDeclaration(s)); return factory.updateSourceFile(node, [ ...otherStatements, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2d15069b6..98668fb87 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import { IoBJson } from '@iobroker/types/build/config'; +import type { IoBJson } from '@iobroker/types/build/config'; const EXIT_CODES = { NO_ERROR: 0, @@ -30,7 +30,7 @@ function getControllerDir(isInstall?: boolean): string { controllerPath = possiblePath; break; } - } catch (e) { + } catch { /* not found */ } } @@ -47,6 +47,11 @@ function getControllerDir(isInstall?: boolean): string { return dirname(controllerPath); } +export const appName = getAppName(); +export const controllerDir = getControllerDir( + typeof process !== 'undefined' && process.argv && process.argv.includes('--install'), +); + /** * Reads controller base settings */ @@ -60,8 +65,5 @@ export function getConfig(): IoBJson { } throw new Error(`Cannot find ${controllerDir}/conf/${appName}.json`); } -export const appName = getAppName(); -export const controllerDir = getControllerDir( - typeof process !== 'undefined' && process.argv && process.argv.includes('--install'), -); + export const Adapter = require(join(controllerDir, 'lib/adapter.js')); diff --git a/src/lib/words.ts b/src/lib/words.ts index 4e8416b9a..5525fef85 100644 --- a/src/lib/words.ts +++ b/src/lib/words.ts @@ -1,5 +1,5 @@ let systemLang: ioBroker.Languages = 'en'; -let systemDictionary: Record = { +const systemDictionary: Record = { 'was not executed, while debug mode is active': { en: 'was not executed, while debug mode is active', de: 'wurde nicht ausgeführt, während der Debug-Modus aktiv ist', @@ -23,7 +23,11 @@ export function getLanguage(): ioBroker.Languages { return systemLang; } -function translateWord(text: string, lang?: ioBroker.Languages, dictionary?: Record): string { +function translateWord( + text: string, + lang?: ioBroker.Languages, + dictionary?: Record, +): string { if (!text) { return ''; } @@ -43,7 +47,7 @@ function translateWord(text: string, lang?: ioBroker.Languages, dictionary?: Rec } else if (typeof text === 'string' && !text.match(/_tooltip$/)) { console.log(`"${text}": {"en": "${text}", "de": "${text}", "ru": "${text}"},`); } else if (typeof text !== 'string') { - console.warn(`Trying to translate non-text:${text}`); + console.warn(`Trying to translate non-text: ${JSON.stringify(text)}`); } return text; } diff --git a/src/main.ts b/src/main.ts index b9e95501e..c8db6640b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,10 +8,10 @@ * Copyright (c) 2014 hobbyquaker */ -import { Script } from 'node:vm'; +import { Script, type ScriptOptions } from 'node:vm'; import { readFileSync, existsSync, statSync, writeFileSync, type Stats } from 'node:fs'; import { join, sep, normalize } from 'node:path'; -import { fork } from 'node:child_process'; +import { fork, type ForkOptions } from 'node:child_process'; import { setTypeScriptResolveOptions, Server } from 'virtual-tsc'; import { isDeepStrictEqual } from 'node:util'; @@ -35,32 +35,43 @@ import * as axios from 'axios'; import * as wake_on_lan from 'wake_on_lan'; import * as nodeSchedule from 'node-schedule'; +import { getAbsoluteDefaultDataDir, Adapter, EXIT_CODES, type AdapterOptions } from '@iobroker/adapter-core'; +import type SentryPlugin from '@iobroker/plugin-sentry'; +import type { GetTimesResult } from 'suncalc'; +import type { CompileResult } from 'virtual-tsc/build/util'; + import { Mirror } from './lib/mirror'; import ProtectFs from './lib/protectFs'; - -import { getAbsoluteDefaultDataDir, Adapter, EXIT_CODES, type AdapterOptions } from '@iobroker/adapter-core'; import { setLanguage, getLanguage } from './lib/words'; import { sandBox } from './lib/sandbox'; import { requestModuleNameByUrl } from './lib/nodeModulesManagement'; -import { createEventObject } from './lib/eventObj'; -import { AstroEventName, Scheduler } from './lib/scheduler'; +import { createEventObject, type EventObj } from './lib/eventObj'; +import { type AstroEventName, Scheduler } from './lib/scheduler'; import { targetTsLib, tsCompilerOptions, jsDeclarationCompilerOptions } from './lib/typescriptSettings'; -import { hashSource, isObject } from './lib/tools'; +import { hashSource } from './lib/tools'; import { resolveTypescriptLibs, resolveTypings, scriptIdToTSFilename, transformScriptBeforeCompilation, transformGlobalDeclarations, -} from '../lib/typescriptTools'; -import { FileSubscriptionResult, JavascriptContext, AdapterConfig, JsScript, ScriptType } from './types'; -import * as ts from 'typescript'; -import { type GetTimesResult } from 'suncalc'; -import { CompileResult } from 'virtual-tsc/build/util'; -import {ScriptOptions} from "vm"; -import {ChildProcess, ForkOptions} from "child_process"; - -const mods: { +} from './lib/typescriptTools'; +import type { + FileSubscriptionResult, + JavascriptContext, + JavaScriptAdapterConfig, + JsScript, + ScriptType, + SubscriptionResult, + SubscribeObject, + JavascriptTimer, + SandboxType, + LogMessage, + DebugState, +} from './types'; +import type { PatternEventCompareFunction } from './lib/patternCompareFunctions'; + +type MODULES = { fs: ProtectFs; dgram: typeof dgram; crypto: typeof crypto; @@ -80,27 +91,6 @@ const mods: { axios: typeof axios; wake_on_lan: typeof wake_on_lan; nodeSchedule: typeof nodeSchedule; -} = { - fs: {} as ProtectFs, - dgram, - crypto, - dns, - events, - http, - https, - http2, - net, - os, - path, - util, - child_process, - stream, - zlib, - - suncalc, - axios, - wake_on_lan, - nodeSchedule, }; /** @@ -120,27 +110,7 @@ const forbiddenMirrorLocations: string[] = [ const packageJson: Record = JSON.parse(readFileSync(`${__dirname}/../package.json`).toString()); const SCRIPT_CODE_MARKER = 'script.js.'; -const stopCounters = {}; -let setStateCountCheckInterval: NodeJS.Timeout | null = null; - let webstormDebug: string | undefined; -let debugMode: string | undefined; - -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; @@ -153,36 +123,6 @@ let tsAmbient: Record; // adapter version and TypeScript version in the hash const tsSourceHashBase = `versions:adapter=${packageJson.version},typescript=${packageJson.dependencies.typescript}`; -let mirror: Mirror | undefined; - -/** if logs are subscribed or not */ -let logSubscribed: boolean = false; - -/** - * @param scriptID - The current script the declarations were generated from - * @param declarations - */ -function provideDeclarationsForGlobalScript(scriptID: string, declarations: string): void { - // 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: Date): number { const fullYear: number = dateInput.getFullYear() | 0; @@ -235,224 +175,13 @@ function dstOffsetAtDate(dateInput: Date): number { ); } -function loadTypeScriptDeclarations(): void { - // 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 ( - typeof (adapter.config as AdapterConfig)?.libraries === 'string' && - typeof (adapter.config as AdapterConfig).libraryTypings === 'string' - ) { - const installedLibs = (adapter.config as AdapterConfig).libraries - .split(/[,;\s]+/) - .map(s => s.trim().split('@')[0]) - .filter(s => !!s); - - const wantsTypings = (adapter.config as AdapterConfig).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, - adapter.getAdapterScopedPackageIdentifier ? adapter.getAdapterScopedPackageIdentifier(pkg) : pkg, - // node needs ambient typings, so we don't wrap it in declare module - pkg !== 'node', - ); - if (!pkgTypings) { - // Create the 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: JavascriptContext = { - 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: {}, - tempDirectories: {}, - folderCreationVerifiedObjects: {}, - updateLogSubscriptions, - convertBackStringifiedValues, - updateObjectContext, - prepareStateObject, - debugMode, - timeSettings: { - format12: false, - leadingZeros: true, - }, - rulesOpened: null, //opened rules - getAbsoluteDefaultDataDir, - adapter: null, - language: 'en', - logError: function (msg: string, e: Error, offs?: number): void { - const stack = e.stack ? e.stack.toString().split('\n') : e ? e.toString() : ''; - if (!msg.includes('\n')) { - msg = msg.replace(/[: ]*$/, ': '); - } - - // errorLogFunction.error(msg + stack[0]); - context.errorLogFunction.error(msg + fixLineNo(stack[0])); - for (let i = offs || 1; i < stack.length; i++) { - if (!stack[i]) { - continue; - } - if (stack[i].match(/runInNewContext|javascript\.js:/)) { - break; - } - // adapter.log.error(fixLineNo(stack[i])); - context.errorLogFunction.error(fixLineNo(stack[i])); - } - }, -}; - -const regExGlobalOld: RegExp = /_global$/; -const regExGlobalNew: RegExp = /script\.js\.global\./; +const regExGlobalOld = /_global$/; +const regExGlobalNew = /script\.js\.global\./; function checkIsGlobal(obj: ioBroker.ScriptObject): boolean { return obj?.common && (regExGlobalOld.test(obj.common.name) || regExGlobalNew.test(obj._id)); } -function convertBackStringifiedValues( - id: string, - state: ioBroker.State | null | undefined, -): ioBroker.State | null | undefined { - if ( - state && - typeof state.val === 'string' && - 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: string, - state: ioBroker.StateValue | ioBroker.SettableState | null, - isAck: boolean | 'true' | 'false', -): ioBroker.State { - let oState: ioBroker.State; - - if (isAck === true || isAck === false || isAck === 'true' || isAck === 'false') { - if (isObject(state) && (state as ioBroker.SettableState).val !== undefined) { - // we assume that we were given a state object if - // state is an object that contains a `val` property - oState = state as ioBroker.State; - if (!Object.prototype.hasOwnProperty.call(oState, 'ack')) { - oState.ack = isAck === true || isAck === 'true'; - } - } else { - // otherwise, assume that the given state is the value to be set - oState = { val: state, ack: isAck === true || isAck === 'true' } as ioBroker.State; - } - } else if (state && isObject(state)) { - oState = state as ioBroker.State; - } else { - oState = { val: state } as ioBroker.State; - } - - if ((adapter.config as AdapterConfig).subscribe) { - return oState as ioBroker.State; - } - // set other values to have a full state object - // mirrors logic from statesInRedis - if (oState.ts === undefined) { - oState.ts = Date.now(); - } - - if (oState.q === undefined) { - oState.q = 0; - } - - oState.from = - typeof oState.from === 'string' && oState.from !== '' ? oState.from : `system.adapter.${adapter.namespace}`; - - if (oState.lc === undefined) { - const formerStateValue = context.interimStateValues[id] || context.states[id]; - if (!formerStateValue) { - oState.lc = oState.ts; - } else { - // isDeepStrictEqual works on objects and primitive values - const hasChanged = !isDeepStrictEqual(formerStateValue.val, oState.val); - if (!formerStateValue.lc || hasChanged) { - oState.lc = oState.ts; - } else { - oState.lc = formerStateValue.lc; - } - } - } - - return oState; -} - function fileMatching(sub: FileSubscriptionResult, id: string, fileName: string): boolean { if (sub.idRegEx) { if (!sub.idRegEx.test(id)) { @@ -476,2559 +205,2907 @@ function fileMatching(sub: FileSubscriptionResult, id: string, fileName: string) return true; } +function getNextTimeEvent(time: string, useNextDay?: boolean): Date { + const now: Date = 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(): Date { + 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: Date): string { + 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 +setTypeScriptResolveOptions({ + paths: [require.resolve('typescript')], +}); + +// compiler instance for global JS declarations +const jsDeclarationServer: Server = new Server(jsDeclarationCompilerOptions, isCI ? false : undefined); /** * Stores the IDs of script objects whose change should be ignored because * the compiled source was just updated */ -const ignoreObjectChange: Set = new Set(); -let objectsInitDone = false; -let statesInitDone = false; +class JavaScript extends Adapter { + public declare config: JavaScriptAdapterConfig; + + private readonly context: JavascriptContext; + + private errorLogFunction: { + error: (msg: string) => void; + warn: (msg: string) => void; + info: (msg: string) => void; + debug: (msg: string) => void; + silly: (msg: string) => void; + }; + + private readonly mods: MODULES; + + private objectsInitDone = false; + private statesInitDone = false; + + private objects: Record = {}; + private states: Record = {}; + private readonly interimStateValues: Record = {}; + private readonly stateIds: string[] = []; + private readonly subscriptions: SubscriptionResult[] = []; + private readonly subscriptionsFile: FileSubscriptionResult[] = []; + private readonly subscriptionsObject: SubscribeObject[] = []; + private readonly subscribedPatterns: Record = {}; + private readonly subscribedPatternsFile: Record = {}; + private readonly adapterSubs: Record = {}; + private readonly timers: { [scriptName: string]: JavascriptTimer[] } = {}; + private readonly _enums: string[] = []; + private readonly names: { [name: string]: string | string[] } = {}; // name: id + private readonly scripts: Record = {}; + private readonly messageBusHandlers: Record< + string, + Record void }[]> + > = {}; + private readonly logSubscriptions: Record< + string, + { + sandbox: SandboxType; + cb: (info: LogMessage) => void; + id: number; + severity: ioBroker.LogLevel | '*'; + }[] + > = {}; + private readonly tempDirectories: { [scriptName: string]: string } = {}; // name: path + private readonly folderCreationVerifiedObjects: Record = {}; + + /** if logs are subscribed or not */ + private logSubscribed = false; + + private timeSettings: { + format12: boolean; + leadingZeros: boolean; + } = { format12: false, leadingZeros: true }; + + private dayScheduleTimer: NodeJS.Timeout | null = null; // schedule for astrological day + private sunScheduleTimer: NodeJS.Timeout | null = null; // schedule for sun moment times + private timeScheduleTimer: NodeJS.Timeout | null = null; // schedule for astrological day + + private activeStr = ''; // enabled state prefix + + private mirror: Mirror | undefined; + + private stopCounters: Record = {}; + + private setStateCountCheckInterval: NodeJS.Timeout | null = null; + + private globalScript = ''; + /** Generated declarations for global TypeScripts */ + private globalDeclarations = ''; + // Remember which definitions the global scripts + // have access to, because it depends on the compilation order + private knownGlobalDeclarationsByScript = {}; + private globalScriptLines = 0; + // compiler instance for typescript + private tsServer: Server; + + private readonly ignoreObjectChange: Set = new Set(); + + private debugState: DebugState = { + scriptName: '', + child: null, + promiseOnEnd: null, + paused: false, + started: 0, + running: false, + }; + + constructor(options: Partial = {}) { + 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: Error): boolean => { + // 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, 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); -let adapter: ioBroker.Adapter; + // signal to the JS-Controller that we handled the error ourselves + return true; + } + } -function startAdapter(options: Partial = {}) { - options = options || {}; - Object.assign(options, { - name: 'javascript', - useFormatDate: true, // load float formatting + return false; + }, + }; - objectChange: (id: string, obj?: ioBroker.Object | null): void => { - // 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; - } + super(options as AdapterOptions); + + 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: {} as ProtectFs, + dgram, + crypto, + dns, + events, + http, + https, + http2, + net, + os, + path, + util, + child_process, + stream, + zlib, + + suncalc, + axios, + wake_on_lan, + nodeSchedule, + }; - // When still in initializing: already remember current values, - // but data structures are initialized elsewhere - if (!objectsInitDone) { - if (obj) { - context.objects[id] = obj; + // check the webstorm debug and just debug modes + let debugMode: string | undefined; + 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]; + } } - return; } + } - if (id.startsWith('enum.')) { - // clear cache - context.cacheObjectEnums = {}; + this.errorLogFunction = this.log; + + 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: this as unknown as ioBroker.Adapter, + logError: this.logError.bind(this), + }; - // 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); - } - } + this.tsServer = new Server(tsCompilerOptions, this.tsLog); + } + + async onObjectChange(id: string, obj?: ioBroker.Object | null): Promise { + // 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 as ioBroker.Object; + 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 (obj && id === 'system.config') { - // set language for debug messages - if (obj.common?.language) { - setLanguage(obj.common.language); + 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); } } + } - // update stored time format for variables.dayTime - if (id === `${adapter.namespace}.variables.dayTime` && obj?.native) { - context.timeSettings.format12 = obj.native.format12 || false; - context.timeSettings.leadingZeros = - obj.native.leadingZeros === undefined ? true : obj.native.leadingZeros; - } + if (id === 'system.config' && obj?.common?.language) { + // set language for debug messages + setLanguage(obj.common.language); + this.language = obj.common.language; + this.context.language = this.language as ioBroker.Languages; + } + + // 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 disk mirror - mirror?.onObjectChange(id, obj as ioBroker.ScriptObject | null); + // send changes to disk mirror + this.mirror?.onObjectChange(id, obj as ioBroker.ScriptObject | null); - const formerObj = context.objects[id]; + const formerObj = this.objects[id]; - updateObjectContext(id, obj); // Update all Meta object data + 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 && obj.type === 'state' && id.startsWith('alias.0.')) { - adapter.getForeignState(id, (err: Error | null, state: ioBroker.State | null | undefined): void => { - if (err) { - return; - } + // 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) { - context.states[id] = state; - } else if (context.states[id] !== undefined) { - delete context.states[id]; + 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: any) { + this.log.error(`Error in callback: ${err.toString()}`); + } } + }); - 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?.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 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); } - }); - // 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) { - 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?.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); + 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 (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 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 { - // 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); + // if (obj.common.source !== formerObj.common.source) { + // Source changed => restart the script + this.stopCounters[id] = this.stopCounters[id] ? this.stopCounters[id] + 1 : 1; + 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]) { + this.loadScriptById(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: string, state?: ioBroker.State | null): void => { - 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; - } + onStateChange(id: string, state?: ioBroker.State | null): void { + 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 === `${adapter.namespace}.debug.to` && state && !state.ack) { - if (!debugMode) { - debugSendToInspector(state.val); - } - 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 (!statesInitDone) { - if (state) { - context.states[id] = state; - } - 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: ioBroker.State | null | undefined = 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 }, - }); - } + const oldState: ioBroker.State | null | undefined = this.states[id]; + if (state) { + if (oldState) { + // enable or disable script + if (!state.ack && id.startsWith(this.activeStr) && this.objects[id]?.native?.script) { + this.extendForeignObject(this.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]); - } + // 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 (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 && */ !context.stateIds.includes(id)) { - context.stateIds.push(id); - context.stateIds.sort(); - } - context.states[id] = state; - } else { - if (oldState) { - delete context.states[id]; - } - state = {} as ioBroker.State; - const pos = context.stateIds.indexOf(id); - if (pos !== -1) { - context.stateIds.splice(pos, 1); } + } else if (/*!oldState && */ !this.stateIds.includes(id)) { + this.stateIds.push(id); + this.stateIds.sort(); } - const _eventObj = createEventObject( - context, - id, - context.convertBackStringifiedValues(id, state), - context.convertBackStringifiedValues(id, oldState), - ); + this.states[id] = state; + } else { + if (oldState) { + delete this.states[id]; + } + state = {} as ioBroker.State; + const pos = this.stateIds.indexOf(id); + if (pos !== -1) { + this.stateIds.splice(pos, 1); + } + } + const _eventObj = createEventObject( + this.context, + id, + this.convertBackStringifiedValues(id, state), + this.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}`); - } + // 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: any) { + this.log.error(`Error in callback: ${err.toString()}`); } } - }, + } + } - fileChange: (id: string, fileName: string, size?: number): void => { - // 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}`); - } + onFileChange(id: string, fileName: string, size: number | null): void { + // 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: any) { + this.log.error(`Error in callback: ${err.toString()}`); } } - }, + } + } - unload: (callback: () => void) => { - debugStop().then(() => { - stopTimeSchedules(); - if (setStateCountCheckInterval) { - clearInterval(setStateCountCheckInterval); - } - stopAllScripts(callback); - }); - }, + async onUnload(callback: () => void): Promise { + await this.debugStop(); + this.stopTimeSchedules(); + if (this.setStateCountCheckInterval) { + clearInterval(this.setStateCountCheckInterval); + this.setStateCountCheckInterval = null; + } + await this.stopAllScripts(); + if (typeof callback === 'function') { + callback(); + } + } - ready: (): void => { - const adapterConfig: AdapterConfig = adapter.config as AdapterConfig; - adapterConfig.maxSetStatePerMinute = - parseInt((adapter.config as AdapterConfig).maxSetStatePerMinute as unknown as string, 10) || 1000; - adapterConfig.maxTriggersPerScript = - parseInt((adapter.config as AdapterConfig).maxTriggersPerScript as unknown as string, 10) || 100; + async onReady(): Promise { + this.config.maxSetStatePerMinute = parseInt(this.config.maxSetStatePerMinute as unknown as string, 10) || 1000; + this.config.maxTriggersPerScript = parseInt(this.config.maxTriggersPerScript as unknown as string, 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 = 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; - } + if (this.supportsFeature && this.supportsFeature('PLUGINS')) { + const sentryInstance: InstanceType = this.getPluginInstance('sentry') as InstanceType< + typeof SentryPlugin + >; + if (sentryInstance) { + const Sentry = sentryInstance.getSentryObject(); + if (Sentry) { + const scope = Sentry.getCurrentScope(); + 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 = 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 did not sort it out, so report it + return event; + } + } - // No exception in it ... do not report - return null; - }); - main(); - }); - } else { - main(); - } - } else { - main(); + // No exception in it ... do not report + return null; + }); } - } else { - main(); } - }, + } - message: (obj: ioBroker.Message) => { - 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 => { - const sandbox = handler.sandbox; + await this.main(); + } + + onMessage(obj: ioBroker.Message): void { + 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 => { + // 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'); + sandbox.verbose && sandbox.log(`onMessage: ${JSON.stringify(obj.message)}`, 'info'); - try { - if (obj.callback) { - handler.cb.call(sandbox, obj.message.data, result => { - sandbox.verbose && - sandbox.log(`onMessage result: ${JSON.stringify(result)}`, 'info'); + try { + if (obj.callback) { + handler.cb.call(sandbox, obj.message.data, result => { + if (sandbox.verbose) { + sandbox.log(`onMessage result: ${JSON.stringify(result)}`, 'info'); + } - adapter.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 (e) { - adapter.setState( - `scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, - true, - true, - ); - context.logError('Error in callback', e); + 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'); + }); } - }); - } - }); - } - break; + } catch (err: unknown) { + this.setState( + `scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, + true, + true, + ); + this.logError('Error in callback', err as Error); + } + }); + } + }); + } + break; - case 'loadTypings': { - // Load typings for the editor - const typings = {}; + 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 */ - } + // try to load TypeScript lib files from disk + try { + const typescriptLibs = resolveTypescriptLibs(targetTsLib); + Object.assign(typings, typescriptLibs); + } catch { + /* ok, no lib then */ + } - // provide the already-loaded ioBroker typings and global script declarations - Object.assign(typings, tsAmbient); + // 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]; - } + // 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) { - adapter.sendTo(obj.from, obj.command, { typings }, obj.callback); - } - break; + if (obj.callback) { + this.sendTo(obj.from, obj.command, { typings }, obj.callback); } + break; + } - case 'calcAstroAll': { - if (obj.message) { - const adapterConfig: AdapterConfig = adapter.config as AdapterConfig; - const sunriseOffset = - parseInt( - obj.message.sunriseOffset === undefined - ? adapterConfig.sunriseOffset - : obj.message.sunriseOffset, - 10, - ) || 0; - const sunsetOffset = - parseInt( - obj.message.sunsetOffset === undefined - ? adapterConfig.sunsetOffset - : obj.message.sunsetOffset, - 10, - ) || 0; - const longitude = - parseFloat( - obj.message.longitude === undefined ? adapterConfig.longitude : obj.message.longitude, - ) || 0; - const latitude = - parseFloat( - obj.message.latitude === undefined ? adapterConfig.latitude : obj.message.latitude, - ) || 0; - const today = getAstroStartOfDay(); - let astroEvents: GetTimesResult & { nextSunrise: Date; nextSunset: Date } = - {} as GetTimesResult & { nextSunrise: Date; nextSunset: Date }; + 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: GetTimesResult & { nextSunrise: Date; nextSunset: Date } = {} as GetTimesResult & { + nextSunrise: Date; + nextSunset: Date; + }; + try { + astroEvents = this.mods.suncalc.getTimes(today, latitude, longitude); + } catch (err: unknown) { + this.log.error(`Cannot calculate astro data: ${err as Error}`); + } + if (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 || adapterConfig.sunriseEvent, - obj.message.sunriseLimitStart || adapterConfig.sunriseLimitStart, - obj.message.sunriseLimitEnd || adapterConfig.sunriseLimitEnd, - sunriseOffset, - false, - latitude, - longitude, - true, - ); - astroEvents.nextSunset = getAstroEvent( - today, - obj.message.sunsetEvent || adapterConfig.sunsetEvent, - obj.message.sunsetLimitStart || adapterConfig.sunsetLimitStart, - obj.message.sunsetLimitEnd || adapterConfig.sunsetLimitEnd, - sunsetOffset, - true, - latitude, - longitude, - true, - ); - } catch (e) { - adapter.log.error(`Cannot calculate astro data: ${e}`); - } + 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: unknown) { + this.log.error(`Cannot calculate astro data: ${err as Error}`); } - - 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', - }; - }); - - obj.callback && adapter.sendTo(obj.from, obj.command, result, obj.callback); } - break; - } - - case 'calcAstro': { - if (obj.message) { - const adapterConfig: AdapterConfig = adapter.config as AdapterConfig; - const longitude = - parseFloat( - obj.message.longitude === undefined ? adapterConfig.longitude : obj.message.longitude, - ) || 0; - const latitude = - parseFloat( - obj.message.latitude === undefined ? adapterConfig.latitude : obj.message.latitude, - ) || 0; - const today = getAstroStartOfDay(); - - const sunriseEvent = obj.message?.sunriseEvent || adapterConfig.sunriseEvent; - const sunriseLimitStart = obj.message?.sunriseLimitStart || adapterConfig.sunriseLimitStart; - const sunriseLimitEnd = obj.message?.sunriseLimitEnd || adapterConfig.sunriseLimitEnd; - const sunriseOffset = - parseInt( - obj.message.sunriseOffset === undefined - ? adapterConfig.sunriseOffset - : obj.message.sunriseOffset, - 10, - ) || 0; - const nextSunrise = getAstroEvent( - today, - sunriseEvent, - sunriseLimitStart, - sunriseLimitEnd, - sunriseOffset, - false, - latitude, - longitude, - true, - ); - - const sunsetEvent = obj.message?.sunsetEvent || adapterConfig.sunsetEvent; - const sunsetLimitStart = obj.message?.sunsetLimitStart || adapterConfig.sunsetLimitStart; - const sunsetLimitEnd = obj.message?.sunsetLimitEnd || adapterConfig.sunsetLimitEnd; - const sunsetOffset = - parseInt( - obj.message.sunsetOffset === undefined - ? adapterConfig.sunsetOffset - : obj.message.sunsetOffset, - 10, - ) || 0; - const nextSunset = getAstroEvent( - today, - sunsetEvent, - sunsetLimitStart, - sunsetLimitEnd, - sunsetOffset, - true, - latitude, - longitude, - true, - ); - const validDateSunrise = nextSunrise !== null && !isNaN(nextSunrise.getTime()); - const validDateSunset = nextSunset !== null && !isNaN(nextSunset.getTime()); + 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()); - adapter.log.debug( - `calcAstro sunrise: ${sunriseEvent} -> start ${sunriseLimitStart}, end: ${sunriseLimitEnd}, offset: ${sunriseOffset} - ${validDateSunrise ? nextSunrise.toISOString() : 'n/a'}`, - ); - adapter.log.debug( - `calcAstro sunset: ${sunsetEvent} -> start ${sunsetLimitStart}, end: ${sunsetLimitEnd}, offset: ${sunsetOffset} - ${validDateSunset ? nextSunset.toISOString() : 'n/a'}`, - ); + result[key] = { + isValidDate: validDate, + serverTime: validDate ? formatHoursMinutesSeconds(astroEvents[key]) : 'n/a', + date: validDate ? astroEvents[key].toISOString() : 'n/a', + }; + }); - obj.callback && - adapter.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, - ); + if (obj.callback) { + this.sendTo(obj.from, obj.command, result, obj.callback); } - break; } + break; + } - case 'debug': { - !debugMode && debugStart(obj.message); - 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, + ); - case 'debugStop': { - !debugMode && debugStop().then(() => console.log('stopped')); - break; - } + 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, + ); - case 'rulesOn': { - context.rulesOpened = obj.message; - console.log(`Enable messaging for ${context.rulesOpened}`); - break; - } + const validDateSunrise = nextSunrise !== null && !isNaN(nextSunrise.getTime()); + const validDateSunset = nextSunset !== null && !isNaN(nextSunset.getTime()); - case 'rulesOff': { - // maybe if (context.rulesOpened === obj.message) - console.log(`Disable messaging for ${context.rulesOpened}`); - context.rulesOpened = null; - break; - } + 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'}`, + ); - case 'getIoBrokerDataDir': { - obj.callback && - adapter.sendTo( + if (obj.callback) { + this.sendTo( obj.from, obj.command, { - dataDir: getAbsoluteDefaultDataDir(), - sep, + 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; + } } + break; } - }, - - /** - * If the JS-Controller catches an unhandled error, this will be called - * so we have a chance to handle it ourselves. - */ - error: (err: Error): boolean | undefined => { - // 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; + case 'debug': { + if (!this.context.debugMode) { + this.debugStart(obj.message); } + break; } - }, - }); - adapter = new Adapter(options); + case 'debugStop': { + if (!this.context.debugMode) { + this.debugStop().then(() => console.log('stopped')); + } + break; + } - // handler for logs - adapter.on('log', (msg: any) => - Object.keys(context.logSubscriptions).forEach((name: string) => - context.logSubscriptions[name].forEach(handler => { + 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: getAbsoluteDefaultDataDir(), + sep, + }, + obj.callback, + ); + } + break; + } + } + } + + onLog(msg: any): void { + Object.keys(this.logSubscriptions).forEach((name: string): void => + this.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; + handler.sandbox.logHandler = undefined; } }), - ), - ); - - context.adapter = adapter; - - return adapter; -} - -function updateObjectContext(id: string, obj: ioBroker.Object | null): void { - 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); - if (pos !== -1) { - context.stateIds.splice(pos, 1); + logError(msg: string, e: Error, offs?: number): void { + const stack = e.stack ? e.stack.toString().split('\n') : e ? e.toString() : ''; + if (!msg.includes('\n')) { + msg = msg.replace(/[: ]*$/, ': '); } - 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); + this.errorLogFunction.error(msg + this.fixLineNo(stack[0])); + for (let i = offs || 1; i < stack.length; i++) { + if (!stack[i]) { + continue; } + if (stack[i].match(/runInNewContext|javascript\.js:/)) { + break; + } + this.errorLogFunction.error(this.fixLineNo(stack[i])); } - - 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; + logWithLineInfo(msg: string): void { + this.errorLogFunction.warn(msg); - const n = getName(id); - let nn = context.objects[id].common ? context.objects[id].common.name : ''; + // get current error stack + const stack = new Error().stack?.split('\n'); - if (nn && typeof nn === 'object') { - nn = nn[getLanguage()] || nn.en; - } - - if (n !== nn) { - if (n) { - removeFromNames(id); - } - if (nn) { - addToNames(obj); + if (stack) { + for (let i = 3; i < stack.length; i++) { + if (!stack[i]) { + continue; + } + if (stack[i].match(/runInContext|runInNewContext|javascript\.js:/)) { + break; + } + this.errorLogFunction.warn(this.fixLineNo(stack[i])); } } } -} - -function main(): void { - !debugMode && patchFont().then(patched => patched && adapter.log.debug('Font patched')); - const adapterConfig: AdapterConfig = adapter.config as AdapterConfig; + async main(): Promise { + // Patch the font as it sometimes is wrong + if (!this.context.debugMode) { + if (await this.patchFont()) { + this.log.debug('Font patched'); + } + } - adapter.log.debug(`config.subscribe (Do not subscribe all states on start): ${adapterConfig.subscribe}`); + this.log.debug(`config.subscribe (Do not subscribe all states on start): ${this.config.subscribe}`); - // correct jsonConfig for admin - adapter.getForeignObject( - `system.adapter.${adapter.namespace}`, - (_err: null | Error, obj: ioBroker.InstanceObject | null) => { - if (obj?.common) { - if (obj.common.adminUI?.config !== 'json') { - if (obj.common.adminUI) { - obj.common.adminUI.config = 'json'; - } else { - obj.common.adminUI = { config: 'json' }; - } - adapter.setForeignObject(obj._id, obj); + // correct jsonConfig for admin + const instObj: ioBroker.InstanceObject | null | undefined = await this.getForeignObjectAsync( + `system.adapter.${this.namespace}`, + ); + if (instObj?.common) { + if (instObj.common.adminUI?.config !== 'json') { + if (instObj.common.adminUI) { + instObj.common.adminUI.config = 'json'; + } else { + instObj.common.adminUI = { config: 'json' }; } - } - }, - ); - - // todo - context.errorLogFunction = webstormDebug - ? { - error: console.error, - warn: console.warn, - info: console.info, - debug: console.log, - silly: console.log, - } - : adapter.log; - activeStr = `${adapter.namespace}.scriptEnabled.`; - - mods.fs = new ProtectFs(adapter.log, getAbsoluteDefaultDataDir()); - mods['fs/promises'] = mods.fs.promises; // to avoid require('fs/promises'); - - // try to read TS declarations - try { - tsAmbient = { - 'javascript.d.ts': 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 an 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); + this.setForeignObject(instObj._id, instObj); } } - // Keep the adapter from crashing when the included typings cannot be read - tsAmbient = {}; - } - function _log(level: ioBroker.LogLevel, msg: string): void { - context.errorLogFunction && context.errorLogFunction[level](msg); + if (webstormDebug) { + this.errorLogFunction = { + error: console.error, + warn: console.warn, + info: console.info, + debug: console.log, + silly: console.log, + }; + this.context.errorLogFunction = this.errorLogFunction; + } + this.activeStr = `${this.namespace}.scriptEnabled.`; - const stack = new Error().stack.split('\n'); + this.mods.fs = new ProtectFs(this.log, getAbsoluteDefaultDataDir()); + this.mods['fs/promises'] = this.mods.fs.promises; // to avoid require('fs/promises'); - for (let i = 3; i < stack.length; i++) { - if (!stack[i]) { - continue; - } - if (stack[i].match(/runInContext|runInNewContext|javascript\.js:/)) { - break; + // try to read TS declarations + try { + tsAmbient = { + 'javascript.d.ts': readFileSync(this.mods.path.join(__dirname, 'lib/javascript.d.ts'), 'utf8'), + }; + this.tsServer.provideAmbientDeclarations(tsAmbient); + jsDeclarationServer.provideAmbientDeclarations(tsAmbient); + } catch (err: unknown) { + this.log.warn(`Could not read TypeScript ambient declarations: ${err as Error}`); + // This should not happen, so send an error report to Sentry + if (this.supportsFeature && this.supportsFeature('PLUGINS')) { + const sentryInstance = this.getPluginInstance('sentry'); + if (sentryInstance) { + const sentryObject = sentryInstance.getSentryObject(); + sentryObject?.captureException(err as Error); + } } - context.errorLogFunction && context.errorLogFunction[level](fixLineNo(stack[i])); + // Keep the adapter from crashing when the included typings cannot be read + tsAmbient = {}; } - } - - context.logWithLineInfo.warn = (msg: string): void => _log('warn', msg); - context.logWithLineInfo.error = (msg: string): void => _log('error', msg); - context.logWithLineInfo.info = (msg: string): void => _log('info', msg); - installLibraries().then(() => { + await this.installLibraries(); // Load the TS declarations for Node.js and all 3rd party modules - loadTypeScriptDeclarations(); - - getData(() => { - context.scheduler = new Scheduler( - adapter.log, - Date, - mods.suncalc, - adapterConfig.latitude, - adapterConfig.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 as AdapterConfig).allowSelfSignedCerts) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - } - - adapter.getObjectView('script', 'javascript', {}, async (err, doc) => { - globalScript = ''; - globalDeclarations = ''; - knownGlobalDeclarationsByScript = {}; - if (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 - // AND the script source, because changing either can change the compilation output - const sourceHash: string = hashSource( - tsSourceHashBase + globalDeclarations + transformedSource, - ); - - let compiled: string | undefined; - let declarations: string | undefined; - // If we already stored the compiled source code and the original source hash, - // use the hash to check whether we can rely on the compiled source code or - // if we need to compile it again - if ( - typeof obj.common.compiled === 'string' && - typeof obj.common.sourceHash === 'string' && - sourceHash === obj.common.sourceHash - ) { - // We can reuse the stored source - compiled = obj.common.compiled; - declarations = obj.common.declarations; - adapter.log.info( - `${obj._id}: source code did not change, using cached compilation result...`, + this.loadTypeScriptDeclarations(); + + await this.getData(); + this.context.scheduler = new Scheduler( + this.log, + Date, + this.mods.suncalc, + this.config.latitude, + this.config.longitude, + ); + await this.dayTimeSchedules(); + await this.sunTimeSchedules(); + await this.timeSchedule(); + + // Warning. It could have a side effect in compact mode, so all adapters will accept self-signed certificates + if (this.config.allowSelfSignedCerts) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + + const doc = await this.getObjectViewAsync('script', 'javascript', {}); + if (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 + this.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 + // AND the script source, because changing either can change the compilation output + const sourceHash: string = hashSource( + tsSourceHashBase + this.globalDeclarations + transformedSource, + ); + + let compiled: string | undefined; + let declarations: string | undefined; + // If we already stored the compiled source code and the original source hash, + // use the hash to check whether we can rely on the compiled source code or + // if we need to compile it again + if ( + typeof obj.common.compiled === 'string' && + typeof obj.common.sourceHash === 'string' && + sourceHash === obj.common.sourceHash + ) { + // We can reuse the stored source + compiled = obj.common.compiled; + declarations = obj.common.declarations; + this.log.info( + `${obj._id}: source code did not change, using cached compilation result...`, + ); + } else { + // We don't have a hashed source code, or the original source changed, compile it + const filename = scriptIdToTSFilename(obj._id); + let tsCompiled: CompileResult; + try { + tsCompiled = this.tsServer.compile(filename, transformedSource); + } catch (err: unknown) { + this.log.error(`${obj._id}: TypeScript compilation failed:\n${err as Error}`); + continue; + } + + const errors = tsCompiled.diagnostics + .map(diag => `${diag.annotatedSource}\n`) + .join('\n'); + + if (tsCompiled.success) { + if (errors.length > 0) { + this.log.warn( + `${obj._id}: TypeScript compilation completed with errors:\n${errors}`, ); } else { - // We don't have a hashed source code, or the original source changed, compile it - const filename = scriptIdToTSFilename(obj._id); - let tsCompiled: CompileResult; - try { - tsCompiled = tsServer.compile(filename, transformedSource); - } catch (e) { - adapter.log.error(`${obj._id}: TypeScript compilation failed:\n${e}`); - continue; - } - - const errors = tsCompiled.diagnostics - .map(diag => `${diag.annotatedSource}\n`) - .join('\n'); - - if (tsCompiled.success) { - if (errors.length > 0) { - adapter.log.warn( - `${obj._id}: TypeScript compilation completed with errors:\n${errors}`, - ); - } else { - adapter.log.info(`${obj._id}: TypeScript compilation successful`); - } - compiled = tsCompiled.result; - // Global scripts that have been transformed to support `import` need to have their declarations transformed aswell - declarations = transformGlobalDeclarations( - tsCompiled.declarations || '', - ); - - const newCommon: { - compiled: string | undefined; - declarations?: string; - sourceHash: string; - } = { - sourceHash, - compiled, - }; - if (declarations) { - newCommon.declarations = declarations; - } - - // Store the compiled source and the original source hash, so we don't need to do the work again next time - ignoreObjectChange.add(obj._id); // ignore the next change and don't restart scripts - adapter.extendForeignObject(obj._id, { - common: newCommon, - }); - } else { - adapter.log.error( - `${obj._id}: TypeScript compilation failed:\n${errors}`, - ); - continue; - } + this.log.info(`${obj._id}: TypeScript compilation successful`); } - globalScript += compiled + '\n'; - // if declarations were generated, remember them - if (declarations != null) { - provideDeclarationsForGlobalScript(obj._id, declarations); + compiled = tsCompiled.result; + // Global scripts that have been transformed to support `import` need to have their declarations transformed aswell + declarations = transformGlobalDeclarations(tsCompiled.declarations || ''); + + const newCommon: { + compiled: string | undefined; + declarations?: string; + sourceHash: string; + } = { + sourceHash, + compiled, + }; + if (declarations) { + newCommon.declarations = declarations; } + + // Store the compiled source and the original source hash, so we don't need to do the work again next time + this.ignoreObjectChange.add(obj._id); // ignore the next change and don't restart scripts + this.extendForeignObject(obj._id, { + common: newCommon, + }); } else { - // javascript - const sourceCode = obj.common.source; - globalScript += sourceCode + '\n'; - - // try to compile the declarations so TypeScripts can use - // functions defined in global JavaScripts - const filename = scriptIdToTSFilename(obj._id); - let tsCompiled: CompileResult; - try { - tsCompiled = jsDeclarationServer.compile(filename, sourceCode); - } catch (e) { - adapter.log.warn( - `${obj._id}: Error while generating type declarations, skipping:\n${e}`, - ); - continue; - } - // if declarations were generated, remember them - if (tsCompiled.success && tsCompiled.declarations != null) { - provideDeclarationsForGlobalScript(obj._id, tsCompiled.declarations); - } + this.log.error(`${obj._id}: TypeScript compilation failed:\n${errors}`); + continue; } } + this.globalScript += `${compiled}\n`; + // if declarations were generated, remember them + if (declarations != null) { + this.provideDeclarationsForGlobalScript(obj._id, declarations); + } + } else { + // javascript + const sourceCode = obj.common.source; + this.globalScript += `${sourceCode}\n`; + + // try to compile the declarations so TypeScripts can use + // functions defined in global JavaScripts + const filename = scriptIdToTSFilename(obj._id); + let tsCompiled: CompileResult; + try { + tsCompiled = jsDeclarationServer.compile(filename, sourceCode); + } catch (err: unknown) { + this.log.warn( + `${obj._id}: Error while generating type declarations, skipping:\n${err as Error}`, + ); + continue; + } + // if declarations were generated, remember them + if (tsCompiled.success && tsCompiled.declarations != null) { + this.provideDeclarationsForGlobalScript(obj._id, tsCompiled.declarations); + } } } } } + } + } - globalScript = globalScript.replace(/\r\n/g, '\n'); - globalScriptLines = globalScript.split(/\n/g).length - 1; + this.globalScript = this.globalScript.replace(/\r\n/g, '\n'); + this.globalScriptLines = this.globalScript.split(/\n/g).length - 1; - if (doc && doc.rows && doc.rows.length) { - // load all scripts - for (let i = 0; i < doc.rows.length; i++) { - if (!checkIsGlobal(doc.rows[i].value)) { - load(doc.rows[i].value); - } - } + if (doc?.rows?.length) { + // load all scripts + for (let i = 0; i < doc.rows.length; i++) { + if (!checkIsGlobal(doc.rows[i].value)) { + this.loadScript(doc.rows[i].value); } + } + } - if (adapterConfig.mirrorPath) { - adapterConfig.mirrorInstance = parseInt(adapterConfig.mirrorInstance as unknown as string, 10) || 0; - if (adapter.instance === adapterConfig.mirrorInstance) { - const ioBDataDir = getAbsoluteDefaultDataDir() + sep; - adapterConfig.mirrorPath = normalize(adapterConfig.mirrorPath); - let mirrorForbidden = false; - for (let dir of forbiddenMirrorLocations) { - dir = join(ioBDataDir, dir) + sep; - if (dir.includes(adapterConfig.mirrorPath) || adapterConfig.mirrorPath.startsWith(dir)) { - adapter.log.error( - `The Mirror directory is not allowed to be a central ioBroker directory!`, - ); - adapter.log.error( - `Directory ${adapterConfig.mirrorPath} is not allowed to mirror files!`, - ); - mirrorForbidden = true; - break; - } - } - if (!mirrorForbidden) { - mirror = new Mirror({ - adapter, - log: adapter.log, - diskRoot: adapterConfig.mirrorPath, - }); - } + if (this.config.mirrorPath) { + this.config.mirrorInstance = parseInt(this.config.mirrorInstance as unknown as string, 10) || 0; + if (this.instance === this.config.mirrorInstance) { + const ioBDataDir = getAbsoluteDefaultDataDir() + sep; + this.config.mirrorPath = normalize(this.config.mirrorPath); + let mirrorForbidden = false; + for (let dir of forbiddenMirrorLocations) { + dir = join(ioBDataDir, dir) + sep; + if (dir.includes(this.config.mirrorPath) || this.config.mirrorPath.startsWith(dir)) { + this.log.error(`The Mirror directory is not allowed to be a central ioBroker directory!`); + this.log.error(`Directory ${this.config.mirrorPath} is not allowed to mirror files!`); + mirrorForbidden = true; + break; } } - - // CHeck setState counter per minute and stop script if too high - setStateCountCheckInterval = setInterval(() => { - Object.keys(context.scripts).forEach(id => { - if (!context.scripts[id]) { - return; - } - const currentSetStatePerMinuteCounter = context.scripts[id].setStatePerMinuteCounter; - context.scripts[id].setStatePerMinuteCounter = 0; - if (currentSetStatePerMinuteCounter > adapterConfig.maxSetStatePerMinute) { - context.scripts[id].setStatePerMinuteProblemCounter++; - adapter.log.debug( - `Script ${id} has reached the maximum of ${adapterConfig.maxSetStatePerMinute} setState calls per minute in ${context.scripts[id].setStatePerMinuteProblemCounter} consecutive minutes`, - ); - // Allow "too high counters" for 1 minute for script starts or such and only - // stop the script when lasts longer - if (context.scripts[id].setStatePerMinuteProblemCounter > 1) { - adapter.log.error( - `Script ${id} is calling setState more than ${adapterConfig.maxSetStatePerMinute} times per minute! Stopping Script now! Please check your script!`, - ); - stop(id); - } - } else if (context.scripts[id].setStatePerMinuteProblemCounter > 0) { - context.scripts[id].setStatePerMinuteProblemCounter--; - adapter.log.debug( - `Script ${id} has NOT reached the maximum of ${adapterConfig.maxSetStatePerMinute} setState calls per minute. Decrease problem counter to ${context.scripts[id].setStatePerMinuteProblemCounter}`, - ); - } + if (!mirrorForbidden) { + this.mirror = new Mirror({ + adapter: this, + log: this.log, + diskRoot: this.config.mirrorPath, }); - }, 60000); - }); - }); - }); -} - -function stopAllScripts(cb) { - Object.keys(context.scripts).forEach(id => stop(id)); - setTimeout(() => cb(), 0); -} + } + } + } -let globalScript = ''; -/** Generated declarations for global TypeScripts */ -let globalDeclarations = ''; -// Remember which definitions the global scripts -// have access to, because it depends on the compile order -let knownGlobalDeclarationsByScript = {}; -let globalScriptLines = 0; -let activeStr = ''; // enabled state prefix -let dayScheduleTimer = null; // schedule for astrological day -let sunScheduleTimer = null; // schedule for sun moment times -let timeScheduleTimer = null; // schedule for astrological day + // CHeck setState counter per minute and stop script if too high + this.setStateCountCheckInterval = setInterval(() => { + Object.keys(this.scripts).forEach(id => { + if (!this.scripts[id]) { + return; + } + const currentSetStatePerMinuteCounter = this.scripts[id].setStatePerMinuteCounter; + this.scripts[id].setStatePerMinuteCounter = 0; + if (currentSetStatePerMinuteCounter > this.config.maxSetStatePerMinute) { + this.scripts[id].setStatePerMinuteProblemCounter++; + this.log.debug( + `Script ${id} has reached the maximum of ${this.config.maxSetStatePerMinute} setState calls per minute in ${this.scripts[id].setStatePerMinuteProblemCounter} consecutive minutes`, + ); + // Allow "too high counters" for 1 minute for script starts or such and only + // stop the script when lasts longer + if (this.scripts[id].setStatePerMinuteProblemCounter > 1) { + this.log.error( + `Script ${id} is calling setState more than ${this.config.maxSetStatePerMinute} times per minute! Stopping Script now! Please check your script!`, + ); + this.stopScript(id); + } + } else if (this.scripts[id].setStatePerMinuteProblemCounter > 0) { + this.scripts[id].setStatePerMinuteProblemCounter--; + this.log.debug( + `Script ${id} has NOT reached the maximum of ${this.config.maxSetStatePerMinute} setState calls per minute. Decrease problem counter to ${this.scripts[id].setStatePerMinuteProblemCounter}`, + ); + } + }); + }, 60000); + } + + private loadTypeScriptDeclarations(): void { + // 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 (typeof this.config?.libraries === 'string' && typeof this.config.libraryTypings === 'string') { + const installedLibs = this.config.libraries + .split(/[,;\s]+/) + .map(s => s.trim().split('@')[0]) + .filter(s => !!s); + + const wantsTypings = this.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 submodules (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('/')); -function getNextTimeEvent(time: string, useNextDay?: boolean): Date { - const now: Date = getAstroStartOfDay(); - let [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); + if (installedLibs.includes(pkgName) && !packages.includes(lib)) { + packages.push(lib); + } + } + } + for (const pkg of packages) { + let pkgTypings = resolveTypings( + pkg, + this.getAdapterScopedPackageIdentifier ? this.getAdapterScopedPackageIdentifier(pkg) : pkg, + // node needs ambient typings, so we don't wrap it in declare module + pkg !== 'node', + ); + if (!pkgTypings) { + // Create the 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}";`, + }; + } + this.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 + this.tsServer.provideAmbientDeclarations(pkgTypings); + jsDeclarationServer.provideAmbientDeclarations(pkgTypings); + } } - now.setHours(nTimeHours); - now.setMinutes(nTimeMinutes); + updateObjectContext(id: string, obj: ioBroker.Object | null | undefined): void { + if (obj) { + // add state to state ID's list + if (obj.type === 'state') { + if (!this.stateIds.includes(id)) { + this.stateIds.push(id); + this.stateIds.sort(); + } + if (this.context.devices && this.context.channels) { + const parts = id.split('.'); + parts.pop(); + const chn = parts.join('.'); + this.context.channels[chn] = this.context.channels[chn] || []; + this.context.channels[chn].push(id); + + parts.pop(); + const dev = parts.join('.'); + this.context.devices[dev] = this.context.devices[dev] || []; + this.context.devices[dev].push(id); + } + } + } else { + // delete object from state ID's list + const pos = this.stateIds.indexOf(id); + if (pos !== -1) { + this.stateIds.splice(pos, 1); + } + if (this.context.devices && this.context.channels) { + const parts = id.split('.'); + parts.pop(); + const chn = parts.join('.'); + if (this.context.channels[chn]) { + const posChn = this.context.channels[chn].indexOf(id); + posChn !== -1 && this.context.channels[chn].splice(posChn, 1); + } - return now; -} + parts.pop(); + const dev = parts.join('.'); + if (this.context.devices[dev]) { + const posDev = this.context.devices[dev].indexOf(id); + posDev !== -1 && this.context.devices[dev].splice(posDev, 1); + } + } -function getAstroEvent( - date: Date, - astroEvent: AstroEventName, - start: string, - end: string, - offsetMinutes: number | string, - isDayEnd: boolean, - latitude: number, - longitude: number, - useNextDay?: boolean, -): Date { - let ts: Date = mods.suncalc.getTimes(date, latitude, longitude)[astroEvent]; - if (!ts || ts.getTime().toString() === 'NaN') { - ts = isDayEnd ? getNextTimeEvent(end, useNextDay) : getNextTimeEvent(start, useNextDay); - } - ts.setMilliseconds(0); - ts.setMinutes(ts.getMinutes() + (parseInt(offsetMinutes as unknown as string, 10) || 0)); + delete this.folderCreationVerifiedObjects[id]; + } - let [timeHoursStart, timeMinutesStart] = start.split(':'); - const nTimeHoursStart = parseInt(timeHoursStart, 10); - const nTimeMinutesStart = parseInt(timeMinutesStart, 10) || 0; + if (!obj && this.objects[id]) { + // objects was deleted + this.removeFromNames(id); + delete this.objects[id]; + } else if (obj && !this.objects[id]) { + // object was added + this.objects[id] = obj; + this.addToNames(obj); + } else if (obj && this.objects[id].common) { + // Object just changed + this.objects[id] = obj; - if (ts.getHours() < nTimeHoursStart || (ts.getHours() === nTimeHoursStart && ts.getMinutes() < nTimeMinutesStart)) { - ts = getNextTimeEvent(start, useNextDay); - ts.setSeconds(0); - } + const n = this.getName(id); + let nn = this.objects[id].common ? this.objects[id].common.name : ''; - let [timeHoursEnd, timeMinutesEnd] = end.split(':'); - const nTimeHoursEnd = parseInt(timeHoursEnd, 10); - const nTimeMinutesEnd = parseInt(timeMinutesEnd, 10) || 0; + if (nn && typeof nn === 'object') { + nn = nn[getLanguage()] || nn.en; + } - if (ts.getHours() > nTimeHoursEnd || (ts.getHours() === nTimeHoursEnd && ts.getMinutes() > nTimeMinutesEnd)) { - ts = getNextTimeEvent(end, useNextDay); - ts.setSeconds(0); + if (n !== nn) { + if (n) { + this.removeFromNames(id); + } + if (nn) { + this.addToNames(obj); + } + } + } } - // if event in the past - if (date > ts && useNextDay) { - // take the next day - ts.setDate(ts.getDate() + 1); + async stopAllScripts(): Promise { + const scripts = Object.keys(this.scripts); + const promises: Promise[] = []; + for (let i = 0; i < scripts.length; i++) { + promises.push(this.stopScript(scripts[i])); + } + return Promise.all(promises).then(() => {}); } - return ts; -} -function timeSchedule(adapter: ioBroker.Adapter, context: JavascriptContext): void { - const now = new Date(); - let hours = now.getHours(); - const minutes = now.getMinutes(); - if (context.timeSettings.format12) { - if (hours > 12) { - hours -= 12; + convertBackStringifiedValues( + id: string, + state: ioBroker.State | null | undefined, + ): ioBroker.State | null | undefined { + if ( + state && + typeof state.val === 'string' && + this.objects[id]?.common && + (this.objects[id].common.type === 'array' || this.objects[id].common.type === 'object') + ) { + try { + state.val = JSON.parse(state.val); + } catch (err: any) { + if (id.startsWith('javascript.') || id.startsWith('0_userdata.0')) { + this.log.info( + `Could not parse value for id ${id} into ${this.objects[id].common.type}: ${err.toString()}`, + ); + } else { + this.log.debug( + `Could not parse value for id ${id} into ${this.objects[id].common.type}: ${err.toString()}`, + ); + } + } } + return state; } - let sHours: string; - if (context.timeSettings.leadingZeros) { - sHours = hours.toString().padStart(2, '0'); - } else { - sHours = hours.toString(); - } - - adapter.setState('variables.dayTime', { val: `${sHours}:${minutes.toString().padStart(2, '0')}`, ack: true }); - now.setMinutes(now.getMinutes() + 1); - now.setSeconds(0); - now.setMilliseconds(0); - const interval = now.getTime() - Date.now(); - timeScheduleTimer = setTimeout(timeSchedule, interval, adapter, context); -} + prepareStateObjectSimple(id: string, state: ioBroker.StateValue, isAck: boolean): ioBroker.State { + let oState: ioBroker.State; -function dayTimeSchedules(adapter: ioBroker.Adapter, context: JavascriptContext): void { - const adapterConfig: AdapterConfig = adapter.config as AdapterConfig; - // get astrological event - if ( - adapterConfig.latitude === undefined || - adapterConfig.longitude === undefined || - (adapterConfig.latitude as unknown as string) === '' || - (adapterConfig.longitude as unknown as string) === '' || - adapterConfig.latitude === null || - adapterConfig.longitude === null - ) { - adapter.log.error('Longitude or latitude does not set. Cannot use astro.'); - return; - } - - // Calculate the next event today - const todayDate = getAstroStartOfDay(); - const nowDate = new Date(); - - const todaySunrise = getAstroEvent( - todayDate, - adapterConfig.sunriseEvent, - adapterConfig.sunriseLimitStart, - adapterConfig.sunriseLimitEnd, - adapterConfig.sunriseOffset, - false, - adapterConfig.latitude, - adapterConfig.longitude, - ); - const todaySunset = getAstroEvent( - todayDate, - adapterConfig.sunsetEvent, - adapterConfig.sunsetLimitStart, - adapterConfig.sunsetLimitEnd, - adapterConfig.sunsetOffset, - true, - adapterConfig.latitude, - adapterConfig.longitude, - ); + if (typeof isAck === 'boolean') { + // otherwise, assume that the given state is the value to be set + oState = { val: state, ack: !!isAck } as ioBroker.State; + } else { + oState = { val: state } as ioBroker.State; + } - // Sunrise - let sunriseTimeout = todaySunrise.getTime() - nowDate.getTime(); - if (sunriseTimeout < 0 || sunriseTimeout > 3600000) { - sunriseTimeout = 3600000; + return this.prepareStateObject(id, oState); } - // Sunset - let sunsetTimeout = todaySunset.getTime() - nowDate.getTime(); - if (sunsetTimeout < 0 || sunsetTimeout > 3600000) { - sunsetTimeout = 3600000; - } + prepareStateObject(id: string, state: ioBroker.SettableState | null): ioBroker.State { + let oState: ioBroker.State; - adapter.getState('variables.isDayTime', (err, state) => { - let isDay: boolean; - if (sunriseTimeout < 5000) { - isDay = true; - } else if (sunsetTimeout < 5000) { - isDay = false; + if (state && typeof state === 'object') { + oState = state as ioBroker.State; } else { - // check if in between - isDay = nowDate.getTime() > todaySunrise.getTime() - 60000 && nowDate <= todaySunset; + oState = { val: null } as ioBroker.State; } - const val = state ? !!state.val : false; - if (val !== isDay || state === null) { - adapter.setState('variables.isDayTime', { val: isDay, ack: true }); + if (this.config.subscribe) { + return oState; } - }); - - adapter.getState('variables.isDaylightSaving', (err, state) => { - const isDayLightSaving = dstOffsetAtDate(nowDate) !== 0; - const val = state ? !!state.val : false; - - if (val !== isDayLightSaving || state === null) { - adapter.setState('variables.isDaylightSaving', { val: isDayLightSaving, ack: true }); + // set other values to have a full state object + // mirrors logic from statesInRedis + if (oState.ts === undefined) { + oState.ts = Date.now(); } - }); - - let nextTimeout = sunriseTimeout; - if (sunriseTimeout > sunsetTimeout) { - nextTimeout = sunsetTimeout; - } - nextTimeout = nextTimeout - 3000; - if (nextTimeout < 3000) { - nextTimeout = 3000; - } - dayScheduleTimer = setTimeout(dayTimeSchedules, nextTimeout, adapter, context); -} + if (oState.q === undefined) { + oState.q = 0; + } -function getAstroStartOfDay(): Date { - const d = new Date(); - d.setMinutes(0); - d.setSeconds(0); - d.setMilliseconds(0); - d.setTime(d.getTime() - d.getTimezoneOffset() * 60 * 1000); - d.setUTCHours(0); + oState.from = + typeof oState.from === 'string' && oState.from !== '' ? oState.from : `system.adapter.${this.namespace}`; - return d; -} + if (oState.lc === undefined) { + const formerStateValue = this.interimStateValues[id] || this.states[id]; + if (!formerStateValue) { + oState.lc = oState.ts; + } else { + // isDeepStrictEqual works on objects and primitive values + const hasChanged = !isDeepStrictEqual(formerStateValue.val, oState.val); + if (!formerStateValue.lc || hasChanged) { + oState.lc = oState.ts; + } else { + oState.lc = formerStateValue.lc; + } + } + } -function formatHoursMinutesSeconds(date: Date): string { - const h = String(date.getHours()); - const m = String(date.getMinutes()); - const s = String(date.getSeconds()); + return oState; + } - return `${h.padStart(2, '0')}:${m.padStart(2, '0')}:${s.padStart(2, '0')}`; -} + async getData(): Promise { + await this.subscribeForeignObjectsAsync('*'); -async function sunTimeSchedules(adapter: ioBroker.Adapter, context: JavascriptContext): Promise { - const adapterConfig: AdapterConfig = adapter.config as AdapterConfig; - if (adapterConfig.createAstroStates) { - if (!isNaN(adapterConfig.longitude) && !isNaN(adapterConfig.longitude)) { - const calcDate = getAstroStartOfDay(); + if (!this.config.subscribe) { + await this.subscribeForeignStatesAsync('*'); + } else { + await this.subscribeStatesAsync('debug.to'); + await this.subscribeStatesAsync('scriptEnabled.*'); + } - const times = mods.suncalc.getTimes(calcDate, adapterConfig.latitude, adapterConfig.longitude); + this.log.info('requesting all states'); - adapter.log.debug(`[sunTimeSchedules] Times: ${JSON.stringify(times)}`); + const statesPromise = this.getForeignStatesAsync('*') + .then(res => { + if (!res) { + this.log.error(`Could not initialize states: no result`); + this.terminate(EXIT_CODES.START_IMMEDIATELY_AFTER_STOP); + return; + } + if (!this.config.subscribe) { + this.states = Object.assign(res, this.states); + this.context.states = this.states; - for (const t in times) { - try { - const objId = `variables.astro.${t}`; - - await adapter.setObjectNotExistsAsync(objId, { - type: 'state', - common: { - name: `Astro ${t}`, - type: 'string', - role: 'value', - read: true, - write: false, - }, - native: {}, - }); + this.addGetProperty(this.states); + } - if (times[t] !== null && !isNaN(times[t].getTime())) { - const timeFormatted = formatHoursMinutesSeconds(times[t]); - await adapter.setState(objId, { - val: timeFormatted, - c: times[t].toISOString(), - ack: true, - }); - } else { - await adapter.setState(objId, { val: null, c: 'n/a', ack: true, q: 0x01 }); + // remember all IDs + for (const id in res) { + if (Object.prototype.hasOwnProperty.call(res, id)) { + this.stateIds.push(id); } - } catch (err) { - adapter.log.error( - `[sunTimeSchedules] Unable to set state for astro time "${t}" (${times[t].getTime()}): ${err}`, - ); } - } + this.statesInitDone = true; + this.log.info('received all states'); + }) + .catch((err: any) => { + this.log.error(`Could not initialize states: ${err?.message || 'no result'}`); + this.terminate(EXIT_CODES.START_IMMEDIATELY_AFTER_STOP); + }); - const todayDate = new Date(); - todayDate.setHours(0); - todayDate.setMinutes(0); - todayDate.setSeconds(1); - todayDate.setMilliseconds(0); - todayDate.setDate(todayDate.getDate() + 1); + this.log.info('requesting all objects'); - adapter.log.debug(`[sunTimeSchedules] Next: ${todayDate.toISOString()}`); - sunScheduleTimer = setTimeout(sunTimeSchedules, todayDate.getTime() - Date.now(), adapter, context); - } - } else { - // remove astro states if disabled - adapter.delObject('variables.astro', { recursive: true }); - } -} + const objectsPromise = this.getObjectListAsync({ include_docs: true }) + .then(res => { + if (!res?.rows) { + this.log.error(`Could not initialize objects: no result`); + this.terminate(EXIT_CODES.START_IMMEDIATELY_AFTER_STOP); + return; + } + this.objects = {}; + this.context.objects = this.objects; + for (let i = 0; i < res.rows.length; i++) { + if (!res.rows[i].doc) { + this.log.debug(`Got empty object for index ${i} (${res.rows[i].id})`); + continue; + } + if (this.objects[res.rows[i].doc._id] === undefined) { + // If was already there ignore + this.objects[res.rows[i].doc._id] = res.rows[i].doc; + } + this.objects[res.rows[i].doc._id].type === 'enum' && this._enums.push(res.rows[i].doc._id); -function stopTimeSchedules(): void { - dayScheduleTimer && clearTimeout(dayScheduleTimer); - dayScheduleTimer = null; - sunScheduleTimer && clearTimeout(sunScheduleTimer); - sunScheduleTimer = null; - timeScheduleTimer && clearTimeout(timeScheduleTimer); - timeScheduleTimer = null; -} + // Collect all names + this.addToNames(this.objects[res.rows[i].doc._id]); + } + this.addGetProperty(this.objects); -/** - * Redirects the virtual-tsc log output to the ioBroker log - * @param msg message - * @param sev severity - */ -function tsLog(msg: string, sev: ioBroker.LogLevel): void { - // shift the severities around, we don't care about the small details - if (sev == null || sev === 'info') { - sev = 'debug'; - } else if (sev === 'debug') { - // Don't spam build logs on Travis - if (isCI) return; - sev = 'silly'; - } + const systemConfig = this.objects['system.config']; - if (adapter && adapter.log) { - adapter.log[sev](msg); - } else { - console.log(`[${sev.toUpperCase()}] ${msg}`); - } -} -// 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 -setTypeScriptResolveOptions({ - paths: [require.resolve('typescript')], -}); -// compiler instance for typescript -const tsServer: Server = new Server(tsCompilerOptions, tsLog); + // set language for debug messages + if (systemConfig?.common?.language) { + setLanguage(systemConfig.common.language); + this.language = systemConfig.common.language; + this.context.language = this.language as ioBroker.Languages; + } else if (this.language) { + setLanguage(this.language); + this.context.language = this.language; + } -// compiler instance for global JS declarations -const jsDeclarationServer: Server = new Server(jsDeclarationCompilerOptions, isCI ? false : undefined); + // try to use system coordinates + if (this.config.useSystemGPS) { + if (systemConfig?.common?.latitude || systemConfig?.common?.longitude) { + this.config.latitude = systemConfig.common.latitude; + this.config.longitude = systemConfig.common.longitude; + } else if (this.latitude && this.longitude) { + this.config.latitude = this.latitude; + this.config.longitude = this.longitude; + } + } + this.config.latitude = parseFloat(this.config.latitude as unknown as string); + this.config.longitude = parseFloat(this.config.longitude as unknown as string); + + if (isNaN(this.config.latitude)) { + this.log.warn(`Configured latitude is not a number - check (instance/system) configuration`); + } else if (this.config.latitude < -90 || this.config.latitude > 90) { + this.log.warn( + `Configured latitude "${this.config.latitude}" is invalid - check (instance/system) configuration`, + ); + } -function addGetProperty(object: Record): void { - try { - Object.defineProperty(object, 'get', { - value: function (id: string): any { - return this[id] || this[`${adapter.namespace}.${id}`]; - }, - enumerable: false, - }); - } catch (e) { - console.error('Cannot install get property'); - } -} + if (isNaN(this.config.longitude)) { + this.log.warn(`Configured longitude is not a number - check (instance/system) configuration`); + } else if (this.config.longitude < -180 || this.config.longitude > 180) { + this.log.warn( + `Configured longitude "${this.config.longitude}" is invalid - check (instance/system) configuration`, + ); + } -function fixLineNo(line: string): string { - if (line.includes('javascript.js:')) { - return line; - } - if (!/script[s]?\.js[.\\/]/.test(line)) { - return line; - } - if (/:([\d]+):/.test(line)) { - line = line.replace( - /:([\d]+):/, - ($0, $1) => `:${$1 > globalScriptLines + 1 ? $1 - globalScriptLines - 1 : $1}:`, - ); // one line for 'async function ()' - } else { - line = line.replace( - /:([\d]+)$/, - ($0, $1) => `:${$1 > globalScriptLines + 1 ? $1 - globalScriptLines - 1 : $1}`, - ); // one line for 'async function ()' - } - return line; -} + this.config.sunriseEvent = this.config.sunriseEvent || 'nightEnd'; + this.config.sunriseOffset = this.config.sunriseOffset || 0; + this.config.sunriseLimitStart = this.config.sunriseLimitStart || '06:00'; + this.config.sunriseLimitEnd = this.config.sunriseLimitEnd || '12:00'; + + this.config.sunsetEvent = this.config.sunsetEvent || 'dusk'; + this.config.sunsetOffset = this.config.sunsetOffset || 0; + this.config.sunsetLimitStart = this.config.sunsetLimitStart || '18:00'; + this.config.sunsetLimitEnd = this.config.sunsetLimitEnd || '23:00'; + + this.objectsInitDone = true; + this.log.info('received all objects'); + }) + .catch((err: any) => { + this.log.error(`Could not initialize objects: ${err?.message || 'no result'}`); + this.terminate(EXIT_CODES.START_IMMEDIATELY_AFTER_STOP); + }); -function createActiveObject(id: string, enabled: boolean, cb?: () => void): void { - const idActive = `${adapter.namespace}.scriptEnabled.${id.substring(SCRIPT_CODE_MARKER.length)}`; - - if (!context.objects[idActive]) { - context.objects[idActive] = { - _id: idActive, - common: { - name: `scriptEnabled.${id.substring(SCRIPT_CODE_MARKER.length)}`, - desc: 'controls script activity', - type: 'boolean', - write: true, - read: true, - role: 'switch.active', - }, - native: { - script: id, - }, - type: 'state', - }; - adapter.setForeignObject(idActive, context.objects[idActive], err => { - if (!err) { - const intermediateStateValue = prepareStateObject(idActive, !!enabled, true); - adapter.setForeignState(idActive, !!enabled, true, () => { - if (enabled && !(adapter.config as AdapterConfig).subscribe) { - context.interimStateValues[idActive] = intermediateStateValue; - } - cb && cb(); - }); - } else if (cb) { - cb(); + return Promise.all([statesPromise, objectsPromise]).then(() => {}); + } + + async createActiveObject(id: string, enabled: boolean): Promise { + const idActive = `${this.namespace}.scriptEnabled.${id.substring(SCRIPT_CODE_MARKER.length)}`; + + if (!this.objects[idActive]) { + this.objects[idActive] = { + _id: idActive, + common: { + name: `scriptEnabled.${id.substring(SCRIPT_CODE_MARKER.length)}`, + desc: 'controls script activity', + type: 'boolean', + write: true, + read: true, + role: 'switch.active', + }, + native: { + script: id, + }, + type: 'state', + }; + try { + this.setForeignObjectAsync(idActive, this.objects[idActive]); + const intermediateStateValue = this.prepareStateObjectSimple(idActive, !!enabled, true); + await this.setForeignStateAsync(idActive, !!enabled, true); + if (enabled && !this.config.subscribe) { + this.interimStateValues[idActive] = intermediateStateValue; + } + } catch { + // ignore } - }); - } else { - adapter.getForeignState(idActive, (err, state) => { + } else { + const state = await this.getForeignStateAsync(idActive); if (state && state.val !== enabled) { - const intermediateStateValue = prepareStateObject(idActive, !!enabled, true); - adapter.setForeignState(idActive, !!enabled, true, () => { - if (enabled && !(adapter.config as AdapterConfig).subscribe) { - context.interimStateValues[id] = intermediateStateValue; - } - cb && cb(); - }); - } else if (cb) { - cb(); + const intermediateStateValue = this.prepareStateObjectSimple(idActive, !!enabled, true); + await this.setForeignStateAsync(idActive, !!enabled, true); + if (enabled && !this.config.subscribe) { + this.interimStateValues[id] = intermediateStateValue; + } } - }); + } } -} -function createProblemObject(id: string, cb?: () => void): void { - const idProblem = `${adapter.namespace}.scriptProblem.${id.substring(SCRIPT_CODE_MARKER.length)}`; - - if (!context.objects[idProblem]) { - context.objects[idProblem] = { - _id: idProblem, - common: { - name: `scriptProblem.${id.substring(SCRIPT_CODE_MARKER.length)}`, - desc: 'Script has a problem', - type: 'boolean', - expert: true, - write: false, - read: true, - role: 'indicator.error', - }, - native: { - script: id, - }, - type: 'state', - }; - adapter.setForeignObject(idProblem, context.objects[idProblem], err => { - if (!err) { - adapter.setForeignState(idProblem, false, true, cb); - } else if (cb) { - cb(); + async createProblemObject(id: string): Promise { + const idProblem = `${this.namespace}.scriptProblem.${id.substring(SCRIPT_CODE_MARKER.length)}`; + + if (!this.objects[idProblem]) { + this.objects[idProblem] = { + _id: idProblem, + common: { + name: `scriptProblem.${id.substring(SCRIPT_CODE_MARKER.length)}`, + desc: 'Script has a problem', + type: 'boolean', + expert: true, + write: false, + read: true, + role: 'indicator.error', + }, + native: { + script: id, + }, + type: 'state', + }; + try { + await this.setForeignObjectAsync(idProblem, this.objects[idProblem]); + await this.setForeignStateAsync(idProblem, false, true); + } catch { + // ignore } - }); - } else { - adapter.getForeignState(idProblem, (err, state) => { + } else { + const state = await this.getForeignStateAsync(idProblem); if (state && state.val !== false) { - adapter.setForeignState(idProblem, false, true, cb); - } else if (cb) { - cb(); + await this.setForeignStateAsync(idProblem, false, true); } - }); + } } -} - -function addToNames(obj: ioBroker.Object): void { - const id = obj._id; - if (obj.common?.name) { - let name = obj.common.name; - if (name && typeof name === 'object') { - name = name[getLanguage()] || name.en; - } - if (!name || typeof name !== 'string') { - // TODO, take name in current language - return; - } + addToNames(obj: ioBroker.Object): void { + const id = obj._id; - if (!context.names[name]) { - context.names[name] = id; - } else { - // convert to array - if (!Array.isArray(context.names[name])) { - context.names[name] = [context.names[name] as string]; + if (obj.common?.name) { + let name = obj.common.name; + if (name && typeof name === 'object') { + name = name[getLanguage()] || name.en; + } + if (!name || typeof name !== 'string') { + // TODO, take name in current language + return; } - (context.names[name] as string[]).push(id); + if (!this.names[name]) { + this.names[name] = id; + } else { + // convert to array + if (!Array.isArray(this.names[name])) { + this.names[name] = [this.names[name] as string]; + } + + (this.names[name] as string[]).push(id); + } } } -} -function removeFromNames(id: string): void { - const n = getName(id); + removeFromNames(id: string): void { + const n = this.getName(id); - if (n) { - if (Array.isArray(context.names[n])) { - const pos = context.names[n].indexOf(id); - if (pos > -1) { - context.names[n].splice(pos, 1); + if (n) { + if (Array.isArray(this.names[n])) { + const pos = this.names[n].indexOf(id); + if (pos > -1) { + this.names[n].splice(pos, 1); - if (context.names[n].length === 1) { - context.names[n] = context.names[n][0]; + if (this.names[n].length === 1) { + this.names[n] = this.names[n][0]; + } } + } else { + delete this.names[n]; } - } else { - delete context.names[n]; } } -} -function getName(id: string): string { - for (const n in context.names) { - if (context.names[n] && Array.isArray(context.names[n])) { - if (context.names[n].includes(id)) { + getName(id: string): string | null { + for (const n in this.names) { + if (this.names[n] && Array.isArray(this.names[n])) { + if (this.names[n].includes(id)) { + return n; + } + } else if (this.names[n] === id) { return n; } - } else if (context.names[n] === id) { - return n; } - } - return null; -} + return null; + } -async function installNpm(npmLib: string): Promise { - return new Promise((resolve, reject) => { - const path = __dirname; + async installNpm(npmLib: string): Promise { + return new Promise((resolve, reject) => { + const path = __dirname; - // Also, set the working directory (cwd) of the process instead of using --prefix - // because that has ugly bugs on Windows - const cmd = `npm install ${npmLib} --omit=dev`; - adapter.log.info(`Installing ${npmLib} into ${__dirname} - cmd: ${cmd}`); + // Also, set the working directory (cwd) of the process instead of using --prefix + // because that has ugly bugs on Windows + const cmd = `npm install ${npmLib} --omit=dev`; + this.log.info(`Installing ${npmLib} into ${__dirname} - cmd: ${cmd}`); - // System call used for update of js-controller itself, - // because during the installation the npm packet will be deleted too, but some files must be loaded even during the installation process. - const child = mods['child_process'].exec(cmd, { - windowsHide: true, - cwd: path, - }); + // System call used for update of js-controller itself, + // because during the installation the npm packet will be deleted too, but some files must be loaded even during the installation process. + const child = this.mods.child_process.exec(cmd, { + windowsHide: true, + cwd: path, + }); - child.stdout && child.stdout.on('data', buf => adapter.log.info(buf.toString('utf8'))); + child.stdout?.on('data', buf => this.log.info(buf.toString('utf8'))); - child.stderr && child.stderr.on('data', buf => adapter.log.error(buf.toString('utf8'))); + child.stderr?.on('data', buf => this.log.error(buf.toString('utf8'))); - child.on('err', err => { - adapter.log.error(`Cannot install ${npmLib}: ${err}`); - reject(npmLib); - }); - child.on('error', err => { - adapter.log.error(`Cannot install ${npmLib}: ${err}`); - reject(npmLib); - }); + child.on('err', err => { + this.log.error(`Cannot install ${npmLib}: ${err}`); + reject(new Error(`Cannot install ${npmLib}: ${err}`)); + }); + child.on('error', err => { + this.log.error(`Cannot install ${npmLib}: ${err}`); + reject(new Error(`Cannot install ${npmLib}: ${err}`)); + }); - child.on('exit', (code /* , signal */) => { - if (code) { - adapter.log.error(`Cannot install ${npmLib}: ${code}`); - reject(code); - } - // command succeeded - resolve(code); + child.on('exit', (code: number /* , signal */) => { + if (code) { + this.log.error(`Cannot install ${npmLib}: ${code}`); + reject(new Error(`Cannot install ${npmLib}: ${code}`)); + } + // command succeeded + resolve(code); + }); }); - }); -} - -async function installLibraries() { - if (typeof (adapter.config as AdapterConfig)?.libraries !== 'string') { - (adapter.config as AdapterConfig).libraries = ''; } - const libraries = (adapter.config as AdapterConfig).libraries.split(/[,;\s]+/).filter(d => d.length > 0); - - adapter.log.debug( - `Custom libraries in config: "${(adapter.config as AdapterConfig).libraries}": ${JSON.stringify(libraries)}`, - ); + async installLibraries(): Promise { + if (typeof this.config?.libraries !== 'string') { + this.config.libraries = ''; + } - let installedNodeModules = []; - const keepModules = []; + const libraries = this.config.libraries.split(/[,;\s]+/).filter(d => d.length > 0); - // js-controller >= 6.x - if (typeof adapter.listInstalledNodeModules === 'function') { - installedNodeModules = await adapter.listInstalledNodeModules(); + this.log.debug(`Custom libraries in config: "${this.config.libraries}": ${JSON.stringify(libraries)}`); - adapter.log.debug(`Found installed libraries: ${JSON.stringify(installedNodeModules)}`); - } + let installedNodeModules: string[] = []; + const keepModules: string[] = []; - for (const lib of libraries) { - let depName = lib; - let version = 'latest'; + // js-controller >= 6.x + if (typeof this.listInstalledNodeModules === 'function') { + installedNodeModules = await this.listInstalledNodeModules(); - if (depName.includes('@') && depName.lastIndexOf('@') > 0) { - const parts = depName.split('@'); - version = parts.pop() ?? 'latest'; - depName = parts.join('@'); + this.log.debug(`Found installed libraries: ${JSON.stringify(installedNodeModules)}`); } - /** The real module name, because the dependency can be an url too */ - let moduleName = depName; + for (const lib of libraries) { + let depName = lib; + let version = 'latest'; - if (URL.canParse(depName)) { - moduleName = await requestModuleNameByUrl(depName); + if (depName.includes('@') && depName.lastIndexOf('@') > 0) { + const parts = depName.split('@'); + version = parts.pop() ?? 'latest'; + depName = parts.join('@'); + } - adapter.log.debug(`Found custom library in config: "${moduleName}@${version}" (from ${depName})`); - } else { - adapter.log.debug(`Found custom library in config: "${moduleName}@${version}"`); - } + /** The real module name, because the dependency can be an url too */ + let moduleName = depName; - keepModules.push(moduleName); + if (URL.canParse(depName)) { + moduleName = await requestModuleNameByUrl(depName); - // js-controller >= 6.x - if (typeof adapter.installNodeModule === 'function') { - try { - const result = await adapter.installNodeModule(depName, { version }); - if (result.success) { - adapter.log.debug(`Installed custom library: "${moduleName}@${version}"`); + this.log.debug(`Found custom library in config: "${moduleName}@${version}" (from ${depName})`); + } else { + this.log.debug(`Found custom library in config: "${moduleName}@${version}"`); + } - const importedModule: any = await adapter.importNodeModule(moduleName); - context.mods[moduleName] = importedModule.default ?? importedModule; - } else { - adapter.log.warn(`Cannot install custom npm package "${moduleName}@${version}"`); + keepModules.push(moduleName); + + // js-controller >= 6.x + if (typeof this.installNodeModule === 'function') { + try { + const result = await this.installNodeModule(depName, { version }); + if (result.success) { + this.log.debug(`Installed custom library: "${moduleName}@${version}"`); + + const importedModule: any = await this.importNodeModule(moduleName); + this.mods[moduleName] = importedModule.default ?? importedModule; + } else { + this.log.warn(`Cannot install custom npm package "${moduleName}@${version}"`); + } + } catch (err: unknown) { + this.log.warn(`Cannot install custom npm package "${moduleName}@${version}": ${err as Error}`); } - } catch (e) { - adapter.log.warn(`Cannot install custom npm package "${moduleName}@${version}": ${e.message}`); - } - } else if (!existsSync(`${__dirname}/node_modules/${depName}/package.json`)) { - // js-controller < 6.x - adapter.log.info(`Installing custom library (legacy mode): "${lib}"`); + } else if (!existsSync(`${__dirname}/node_modules/${depName}/package.json`)) { + // js-controller < 6.x + this.log.info(`Installing custom library (legacy mode): "${lib}"`); - try { - await installNpm(lib); - adapter.log.info(`Installed custom npm package (legacy mode): "${libraries[lib]}"`); - } catch (err) { - adapter.log.warn(`Cannot install custom npm package "${libraries[lib]}" (legacy mode): ${err}`); + try { + await this.installNpm(lib); + this.log.info(`Installed custom npm package (legacy mode): "${libraries[lib]}"`); + } catch (err: any) { + this.log.warn( + `Cannot install custom npm package "${libraries[lib]}" (legacy mode): ${err.toString()}`, + ); + } } } - } - // js-controller >= 6.x - if (typeof adapter.uninstallNodeModule === 'function') { - for (const installedNodeModule of installedNodeModules) { - if (!keepModules.includes(installedNodeModule)) { - try { - await adapter.uninstallNodeModule(installedNodeModule); + // js-controller >= 6.x + if (typeof this.uninstallNodeModule === 'function') { + for (const installedNodeModule of installedNodeModules) { + if (!keepModules.includes(installedNodeModule)) { + try { + await this.uninstallNodeModule(installedNodeModule); - adapter.log.info(`Removed custom npm package: "${installedNodeModule}"`); - } catch (err) { - adapter.log.warn(`Cannot remove custom npm package ${installedNodeModule}: ${err}`); + this.log.info(`Removed custom npm package: "${installedNodeModule}"`); + } catch (err: any) { + this.log.warn(`Cannot remove custom npm package ${installedNodeModule}: ${err.toString()}`); + } } } } } -} -function createVM(source: string, name: string, wrapAsync: boolean): false | JsScript { - if (debugMode && name !== debugMode) { - return false; - } + createVM(source: string, name: string, wrapAsync: boolean): false | JsScript { + if (this.context.debugMode && name !== this.context.debugMode) { + return false; + } - if (!debugMode) { - const logSubscriptionsText = - "\n;\nlog(`registered ${__engine.__subscriptions} subscription${__engine.__subscriptions === 1 ? '' : 's'}," + - " ${__engine.__schedules} schedule${__engine.__schedules === 1 ? '' : 's'}," + - " ${__engine.__subscriptionsMessage} message${__engine.__subscriptionsMessage === 1 ? '' : 's'}," + - " ${__engine.__subscriptionsLog} log${__engine.__subscriptionsLog === 1 ? '' : 's'}" + - " and ${__engine.__subscriptionsFile} file subscription${__engine.__subscriptionsFile === 1 ? '' : 's'}`);\n"; + if (!this.context.debugMode) { + const logSubscriptionsText = + "\n;\nlog(`registered ${__engine.__subscriptions} subscription${__engine.__subscriptions === 1 ? '' : 's'}," + + " ${__engine.__schedules} schedule${__engine.__schedules === 1 ? '' : 's'}," + + " ${__engine.__subscriptionsMessage} message${__engine.__subscriptionsMessage === 1 ? '' : 's'}," + + " ${__engine.__subscriptionsLog} log${__engine.__subscriptionsLog === 1 ? '' : 's'}" + + " and ${__engine.__subscriptionsFile} file subscription${__engine.__subscriptionsFile === 1 ? '' : 's'}`);\n"; - if (wrapAsync) { - source = `(async () => {\n${source}\n${logSubscriptionsText}\n})();`; - } else { - source = `${source}\n${logSubscriptionsText}`; - } - } else { - if (wrapAsync) { - source = `(async () => {debugger;\n${source}\n})();`; + if (wrapAsync) { + source = `(async () => {\n${source}\n${logSubscriptionsText}\n})();`; + } else { + source = `${source}\n${logSubscriptionsText}`; + } } else { - source = `debugger;${source}`; + if (wrapAsync) { + source = `(async () => {debugger;\n${source}\n})();`; + } else { + source = `debugger;${source}`; + } } - } - try { - const options: ScriptOptions = { - filename: name, - // displayErrors: true, - // lineOffset: globalScriptLines - }; - return { - script: new Script(source, options), - } as JsScript; - } catch (e) { - context.logError(`${name} compile failed:\r\nat `, e); - return false; + try { + const options: ScriptOptions = { + filename: name, + // displayErrors: true, + // lineOffset: this.globalScriptLines + }; + return { + script: new Script(source, options), + } as JsScript; + } catch (err: unknown) { + this.logError(`${name} compile failed:\r\nat `, err as Error); + return false; + } } -} -function execute(script: JsScript, name: string, engineType: ScriptType, verbose: boolean, debug: boolean): void { - script.intervals = []; - script.timeouts = []; - script.schedules = []; - script.wizards = []; - script.name = name; - script.engineType = engineType; - script._id = Math.floor(Math.random() * 0xffffffff); - script.subscribes = {}; - script.subscribesFile = {}; - script.setStatePerMinuteCounter = 0; - script.setStatePerMinuteProblemCounter = 0; - adapter.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, { - val: false, - ack: true, - expire: 1000, - }); - - const sandbox = sandBox(script, name, verbose, debug, context); - - try { - script.script.runInNewContext(sandbox, { - filename: name, - displayErrors: true, - // lineOffset: globalScriptLines - }); - } catch (e) { - adapter.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, { - val: true, + execute(script: JsScript, name: string, engineType: ScriptType, verbose: boolean, debug: boolean): void { + script.intervals = []; + script.timeouts = []; + script.schedules = []; + script.wizards = []; + script.name = name; + script.engineType = engineType; + script._id = Math.floor(Math.random() * 0xffffffff); + script.subscribes = {}; + script.subscribesFile = {}; + script.setStatePerMinuteCounter = 0; + script.setStatePerMinuteProblemCounter = 0; + this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, { + val: false, ack: true, - c: 'execute', + expire: 1000, }); - context.logError(name, e); - } -} -function unsubscribe(id: string | RegExp | string[]): void { - if (!id) { - adapter.log.warn('unsubscribe: empty name'); - return; - } + const sandbox = sandBox(script, name, verbose, debug, this.context); - if (Array.isArray(id)) { - id.forEach(sub => unsubscribe(sub)); - return; + try { + script.script.runInNewContext(sandbox, { + filename: name, + displayErrors: true, + // lineOffset: this.globalScriptLines + }); + } catch (err: unknown) { + this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, { + val: true, + ack: true, + c: 'execute', + }); + this.logError(name, err as Error); + } } - if (id.constructor && id.constructor.name === 'RegExp') { - // adapter.log.warn('unsubscribe: todo - process regexp'); - return; - } + unsubscribe(id: string | RegExp | string[]): void { + if (!id) { + this.log.warn('unsubscribe: empty name'); + return; + } + + if (Array.isArray(id)) { + id.forEach(sub => unsubscribe(sub)); + return; + } + + if (id.constructor && id.constructor.name === 'RegExp') { + // adapter.log.warn('unsubscribe: todo - process regexp'); + return; + } - if (typeof id !== 'string') { - adapter.log.error(`unsubscribe: invalid type of id - ${typeof id}`); - return; + if (typeof id !== 'string') { + this.log.error(`unsubscribe: invalid type of id - ${typeof id}`); + return; + } + const parts = id.split('.'); + const _adapter = `system.adapter.${parts[0]}.${parts[1]}`; + if (this.objects[_adapter]?.common?.subscribable) { + const a = `${parts[0]}.${parts[1]}`; + const alive = `system.adapter.${a}.alive`; + if (this.adapterSubs[alive]) { + const pos = this.adapterSubs[alive].indexOf(id); + if (pos !== -1) { + this.adapterSubs[alive].splice(pos, 1); + } + if (!this.adapterSubs[alive].length) { + delete this.adapterSubs[alive]; + } + } + this.sendTo(a, 'unsubscribe', id); + } } - const parts = id.split('.'); - const _adapter = `system.adapter.${parts[0]}.${parts[1]}`; - if ( - context.objects[_adapter] && - context.objects[_adapter].common && - context.objects[_adapter].common.subscribable - ) { - const a = `${parts[0]}.${parts[1]}`; - const alive = `system.adapter.${a}.alive`; - if (context.adapterSubs[alive]) { - const pos = context.adapterSubs[alive].indexOf(id); - if (pos !== -1) { - context.adapterSubs[alive].splice(pos, 1); + + // Analyze if logs are still required or not + updateLogSubscriptions(): void { + let found = false; + // go through all scripts and check if some script still requires logs + Object.keys(this.logSubscriptions).forEach(scriptName => { + if (!this.logSubscriptions?.[scriptName] || !this.logSubscriptions[scriptName].length) { + delete this.logSubscriptions[scriptName]; + } else { + found = true; } - if (!context.adapterSubs[alive].length) { - delete context.adapterSubs[alive]; + }); + + if (this.requireLog) { + if (found && !this.logSubscribed) { + this.logSubscribed = true; + this.requireLog(this.logSubscribed); + this.log.info(`Subscribed to log messages (found logSubscriptions)`); + } else if (!found && this.logSubscribed) { + this.logSubscribed = false; + this.requireLog(this.logSubscribed); + this.log.info(`Unsubscribed from log messages (not found logSubscriptions)`); } } - adapter.sendTo(a, 'unsubscribe', id); } -} -// Analyse if logs are still required or not -function updateLogSubscriptions(): void { - let found = false; - // go through all scripts and check if some script still requires logs - Object.keys(context.logSubscriptions).forEach(scriptName => { - if (!context.logSubscriptions?.[scriptName] || !context.logSubscriptions[scriptName].length) { - delete context.logSubscriptions[scriptName]; - } else { - found = true; - } - }); - - if (found && !logSubscribed) { - logSubscribed = true; - adapter.requireLog(logSubscribed); - adapter.log.info(`Subscribed to log messages (found logSubscriptions)`); - } else if (!found && logSubscribed) { - logSubscribed = false; - adapter.requireLog(logSubscribed); - adapter.log.info(`Unsubscribed from log messages (not found logSubscriptions)`); - } -} + async stopScript(name: string): Promise { + this.log.info(`Stopping script ${name}`); -function stop(name: string, callback?: (success: boolean, name: string) => void): void { - adapter.log.info(`Stopping script ${name}`); + await this.setState(`scriptEnabled.${name.substring(SCRIPT_CODE_MARKER.length)}`, false, true); - adapter.setState(`scriptEnabled.${name.substring(SCRIPT_CODE_MARKER.length)}`, false, true); + if (this.messageBusHandlers[name]) { + delete this.messageBusHandlers[name]; + } - if (context.messageBusHandlers[name]) { - delete context.messageBusHandlers[name]; - } + if (this.tempDirectories[name]) { + try { + this.mods.fs.rmSync(this.tempDirectories[name], { recursive: true }); - if (context.tempDirectories[name]) { - try { - mods.fs.rmSync(context.tempDirectories[name], { recursive: true }); + this.log.debug(`Removed temp directory of ${name}: ${this.tempDirectories[name]}`); + } catch { + this.log.warn(`Unable to remove temp directory of ${name}: ${this.tempDirectories[name]}`); + } - adapter.log.debug(`Removed temp directory of ${name}: ${context.tempDirectories[name]}`); - } catch (err) { - adapter.log.warn(`Unable to remove temp directory of ${name}: ${context.tempDirectories[name]}`); + delete this.tempDirectories[name]; } - delete context.tempDirectories[name]; - } - - if (context.logSubscriptions[name]) { - delete context.logSubscriptions[name]; - updateLogSubscriptions(); - } + if (this.logSubscriptions[name]) { + delete this.logSubscriptions[name]; + this.updateLogSubscriptions(); + } - if (context.scripts[name]) { - // Remove from subscriptions - context.isEnums = false; - if ((adapter.config as AdapterConfig).subscribe) { - // check all subscribed IDs - Object.keys(context.scripts[name].subscribes).forEach(id => { - if (context.subscribedPatterns[id]) { - context.subscribedPatterns[id] -= context.scripts[name].subscribes[id]; - if (context.subscribedPatterns[id] <= 0) { - adapter.unsubscribeForeignStates(id); - delete context.subscribedPatterns[id]; - if (context.states[id]) { - delete context.states[id]; + if (this.scripts[name]) { + // Remove from subscriptions + this.context.isEnums = false; + if (this.config.subscribe) { + // check all subscribed IDs + Object.keys(this.scripts[name].subscribes).forEach(id => { + if (this.subscribedPatterns[id]) { + this.subscribedPatterns[id] -= this.scripts[name].subscribes[id]; + if (this.subscribedPatterns[id] <= 0) { + this.unsubscribeForeignStates(id); + delete this.subscribedPatterns[id]; + if (this.states[id]) { + delete this.states[id]; + } } } + }); + } + + for (let i = this.subscriptions.length - 1; i >= 0; i--) { + if (this.subscriptions[i].name === name) { + const sub = this.subscriptions.splice(i, 1)[0]; + if (sub?.pattern.id) { + this.unsubscribe(sub.pattern.id); + } + } else { + if ( + (!this.context.isEnums && this.subscriptions[i].pattern.enumName) || + this.subscriptions[i].pattern.enumId + ) { + this.context.isEnums = true; + } } - }); - } + } - for (let i = context.subscriptions.length - 1; i >= 0; i--) { - if (context.subscriptions[i].name === name) { - const sub = context.subscriptions.splice(i, 1)[0]; - if (sub) { - unsubscribe(sub.pattern.id); + // check all subscribed files + Object.keys(this.scripts[name].subscribesFile).forEach(key => { + if (this.subscribedPatternsFile[key]) { + this.subscribedPatternsFile[key] -= this.scripts[name].subscribesFile[key]; + if (this.subscribedPatternsFile[key] <= 0) { + const [id, file] = key.split('$%$'); + this.unsubscribeForeignFiles(id, file); + delete this.subscribedPatternsFile[key]; + } } - } else { - if ( - (!context.isEnums && context.subscriptions[i].pattern.enumName) || - context.subscriptions[i].pattern.enumId - ) { - context.isEnums = true; + }); + for (let i = this.subscriptionsFile.length - 1; i >= 0; i--) { + if (this.subscriptionsFile[i].name === name) { + this.subscriptionsFile.splice(i, 1); } } - } - // check all subscribed files - Object.keys(context.scripts[name].subscribesFile).forEach(key => { - if (context.subscribedPatternsFile[key]) { - context.subscribedPatternsFile[key] -= context.scripts[name].subscribesFile[key]; - if (context.subscribedPatternsFile[key] <= 0) { - const [id, file] = key.split('$%$'); - adapter.unsubscribeForeignFiles(id, file); - delete context.subscribedPatternsFile[key]; + for (let i = this.subscriptionsObject.length - 1; i >= 0; i--) { + if (this.subscriptionsObject[i].name === name) { + const sub = this.subscriptionsObject.splice(i, 1)[0]; + if (sub) { + this.unsubscribeForeignObjects(sub.pattern); + } } } - }); - for (let i = context.subscriptionsFile.length - 1; i >= 0; i--) { - if (context.subscriptionsFile[i].name === name) { - context.subscriptionsFile.splice(i, 1)[0]; - } - } - for (let i = context.subscriptionsObject.length - 1; i >= 0; i--) { - if (context.subscriptionsObject[i].name === name) { - const sub = context.subscriptionsObject.splice(i, 1)[0]; - sub && adapter.unsubscribeForeignObjects(sub.pattern); + // Stop all timeouts + for (let i = 0; i < this.scripts[name].timeouts.length; i++) { + clearTimeout(this.scripts[name].timeouts[i]); } - } - - // Stop all timeouts - for (let i = 0; i < context.scripts[name].timeouts.length; i++) { - clearTimeout(context.scripts[name].timeouts[i]); - } - // Stop all intervals - for (let i = 0; i < context.scripts[name].intervals.length; i++) { - clearInterval(context.scripts[name].intervals[i]); - } - // Stop all scheduled jobs - for (let i = 0; i < context.scripts[name].schedules.length; i++) { - if (context.scripts[name].schedules[i]) { - const _name = context.scripts[name].schedules[i].name; - if (!mods.nodeSchedule.cancelJob(context.scripts[name].schedules[i])) { - adapter.log.error(`Error by canceling scheduled job "${_name}"`); + // Stop all intervals + for (let i = 0; i < this.scripts[name].intervals.length; i++) { + clearInterval(this.scripts[name].intervals[i]); + } + // Stop all scheduled jobs + for (let i = 0; i < this.scripts[name].schedules.length; i++) { + if (this.scripts[name].schedules[i]) { + const _name = this.scripts[name].schedules[i].name; + if (!this.mods.nodeSchedule.cancelJob(this.scripts[name].schedules[i])) { + this.log.error(`Error by canceling scheduled job "${_name}"`); + } } } - } - // Stop all time wizards jobs - for (let i = 0; i < context.scripts[name].wizards.length; i++) { - if (context.scripts[name].wizards[i]) { - context.scheduler && context.scheduler.remove(context.scripts[name].wizards[i]); + // Stop all time wizards jobs + if (this.context.scheduler) { + for (let i = 0; i < this.scripts[name].wizards.length; i++) { + if (this.scripts[name].wizards[i]) { + this.context.scheduler.remove(this.scripts[name].wizards[i]); + } + } } - } - // if callback for on stop - if (typeof context.scripts[name].onStopCb === 'function') { - context.scripts[name].onStopTimeout = - parseInt(context.scripts[name].onStopTimeout as unknown as string, 10) || 1000; + // if callback for on stop + if (typeof this.scripts[name].onStopCb === 'function') { + this.scripts[name].onStopTimeout = + parseInt(this.scripts[name].onStopTimeout as unknown as string, 10) || 1000; - let timeout = setTimeout(() => { - if (timeout) { - timeout = null; - delete context.scripts[name]; - typeof callback === 'function' && callback(true, name); - } - }, context.scripts[name].onStopTimeout); + await new Promise(resolve => { + let timeout: NodeJS.Timeout | null = setTimeout(() => { + if (timeout) { + timeout = null; + resolve(true); + } + }, this.scripts[name].onStopTimeout); - try { - context.scripts[name].onStopCb(() => { - if (timeout) { - clearTimeout(timeout); - timeout = null; - delete context.scripts[name]; - typeof callback === 'function' && callback(true, name); + try { + this.scripts[name].onStopCb(() => { + if (timeout) { + clearTimeout(timeout); + timeout = null; + resolve(true); + } + }); + } catch (err: unknown) { + this.log.error(`error in onStop callback: ${err as Error}`); } }); - } catch (e) { - adapter.log.error(`error in onStop callback: ${e}`); } - } else { - delete context.scripts[name]; - typeof callback === 'function' && callback(true, name); - } - } else { - typeof callback === 'function' && callback(false, name); - } -} -function prepareScript(obj: ioBroker.ScriptObject, callback) { - if (obj?.common?.enabled && debugState.scriptName === obj._id) { - const id = obj._id; - return debugStop().then(() => { - adapter.log.info(`Debugging of ${id} was stopped, because started in normal mode`); - prepareScript(obj, callback); - }); + delete this.scripts[name]; + return true; + } + return false; } - if ( - obj?.common && - (obj.common.enabled || debugMode === obj._id) && - obj.common.engine === `system.adapter.${adapter.namespace}` && - obj.common.source - ) { - const name = obj._id; - - const nameId = name.substring(SCRIPT_CODE_MARKER.length); - if (!nameId.length || nameId.endsWith('.')) { - adapter.log.error(`Script name ${name} is invalid!`); - typeof callback === 'function' && callback(false, name); - return; - } - const idActive = `scriptEnabled.${nameId}`; - if (!(adapter.config as AdapterConfig).subscribe) { - context.interimStateValues[idActive] = prepareStateObject(`${adapter.namespace}.${idActive}`, true, true); + async prepareScript(obj: ioBroker.ScriptObject): Promise { + if (obj?.common?.enabled && this.debugState.scriptName === obj._id) { + const id = obj._id; + await this.debugStop(); + this.log.info(`Debugging of ${id} was stopped, because started in normal mode`); + return this.prepareScript(obj); } - adapter.setState(idActive, true, true); - obj.common.engineType = obj.common.engineType || ''; if ( - (obj.common.engineType as ScriptType).toLowerCase().startsWith('javascript') || - (obj.common.engineType as ScriptType) === 'Blockly' || - (obj.common.engineType as ScriptType) === 'Rules' + obj?.common?.source && + (obj.common.enabled || this.context.debugMode === obj._id) && + obj.common.engine === `system.adapter.${this.namespace}` ) { - // Javascript - adapter.log.info(`Start JavaScript ${name} (${obj.common.engineType})`); - - let sourceFn = name; - if (webstormDebug) { - const fn = name.replace(/^script.js./, '').replace(/\./g, '/'); - sourceFn = mods.path.join(webstormDebug, `${fn}.js`); - } - const createdScript = createVM(`${globalScript}\n${obj.common.source}`, sourceFn, true); - if (!createdScript) { - typeof callback === 'function' && callback(false, name); - return; + const name = obj._id; + + const nameId = name.substring(SCRIPT_CODE_MARKER.length); + if (!nameId.length || nameId.endsWith('.')) { + this.log.error(`Script name ${name} is invalid!`); + return false; + } + const idActive = `scriptEnabled.${nameId}`; + if (!this.config.subscribe) { + this.interimStateValues[idActive] = this.prepareStateObjectSimple( + `${this.namespace}.${idActive}`, + true, + true, + ); } - context.scripts[name] = createdScript as JsScript; - execute(context.scripts[name], sourceFn, obj.common.engineType as ScriptType, obj.common.verbose, obj.common.debug); - typeof callback === 'function' && callback(true, name); - } else if (obj.common.engineType.toLowerCase().startsWith('typescript')) { - // TypeScript - adapter.log.info(`Compiling TypeScript source ${name}`); - // The source code must be transformed in order to support top level await - // and to force TypeScript to compile the code as a module - const transformedSource = transformScriptBeforeCompilation(obj.common.source, false); - // We need to hash both global declarations that are known until now - // AND the script source, because changing either can change the compilation output - const sourceHash = hashSource(tsSourceHashBase + globalDeclarations + transformedSource); - - let compiled: string; - // If we already stored the compiled source code and the original source hash, - // use the hash to check whether we can rely on the compiled source code or - // if we need to compile it again + await this.setState(idActive, true, true); + obj.common.engineType = obj.common.engineType || ''; + if ( - typeof obj.common.compiled === 'string' && - typeof obj.common.sourceHash === 'string' && - sourceHash === obj.common.sourceHash + (obj.common.engineType as ScriptType).toLowerCase().startsWith('javascript') || + (obj.common.engineType as ScriptType) === 'Blockly' || + (obj.common.engineType as ScriptType) === 'Rules' ) { - // We can reuse the stored source - compiled = obj.common.compiled; - adapter.log.info(`${name}: source code did not change, using cached compilation result...`); - } else { - // We don't have a hashed source code, or the original source changed, compile it - const filename = scriptIdToTSFilename(name); - let tsCompiled: CompileResult; - try { - tsCompiled = tsServer.compile(filename, transformedSource); - } catch (e) { - adapter.log.error(`${obj._id}: TypeScript compilation failed:\n${e}`); - typeof callback === 'function' && callback(false, name); - return; + // Javascript + this.log.info(`Start JavaScript ${name} (${obj.common.engineType})`); + + let sourceFn = name; + if (webstormDebug) { + const fn = name.replace(/^script.js./, '').replace(/\./g, '/'); + sourceFn = this.mods.path.join(webstormDebug, `${fn}.js`); + } + const createdScript = this.createVM(`${this.globalScript}\n${obj.common.source}`, sourceFn, true); + if (!createdScript) { + return false; } + this.scripts[name] = createdScript; + this.execute( + this.scripts[name], + sourceFn, + obj.common.engineType as ScriptType, + obj.common.verbose, + obj.common.debug, + ); + return true; + } + + if (obj.common.engineType.toLowerCase().startsWith('typescript')) { + // TypeScript + this.log.info(`Compiling TypeScript source ${name}`); + // The source code must be transformed in order to support top level await + // and to force TypeScript to compile the code as a module + const transformedSource = transformScriptBeforeCompilation(obj.common.source, false); + // We need to hash both global declarations that are known until now + // AND the script source, because changing either can change the compilation output + const sourceHash = hashSource(tsSourceHashBase + this.globalDeclarations + transformedSource); + + let compiled: string; + // If we already stored the compiled source code and the original source hash, + // use the hash to check whether we can rely on the compiled source code or + // if we need to compile it again + if ( + typeof obj.common.compiled === 'string' && + typeof obj.common.sourceHash === 'string' && + sourceHash === obj.common.sourceHash + ) { + // We can reuse the stored source + compiled = obj.common.compiled; + this.log.info(`${name}: source code did not change, using cached compilation result...`); + } else { + // We don't have a hashed source code, or the original source changed, compile it + const filename = scriptIdToTSFilename(name); + let tsCompiled: CompileResult; + try { + tsCompiled = this.tsServer.compile(filename, transformedSource); + } catch (err: unknown) { + this.log.error(`${obj._id}: TypeScript compilation failed:\n${err as Error}`); + return false; + } - const errors = tsCompiled.diagnostics.map(diag => `${diag.annotatedSource}\n`).join('\n'); + const errors = tsCompiled.diagnostics.map(diag => `${diag.annotatedSource}\n`).join('\n'); - if (tsCompiled.success) { - if (errors.length > 0) { - adapter.log.warn(`${name}: TypeScript compilation had errors:\n${errors}`); + if (tsCompiled.success) { + if (errors.length > 0) { + this.log.warn(`${name}: TypeScript compilation had errors:\n${errors}`); + } else { + this.log.info(`${name}: TypeScript compilation successful`); + } + compiled = tsCompiled.result || ''; + + // Store the compiled source and the original source hash, so we don't need to do the work again next time + this.ignoreObjectChange.add(name); // ignore the next change and don't restart scripts + await this.extendForeignObjectAsync(name, { + common: { + sourceHash, + compiled, + }, + }); } else { - adapter.log.info(`${name}: TypeScript compilation successful`); + this.log.error(`${name}: TypeScript compilation failed:\n${errors}`); + return false; } - compiled = tsCompiled.result; - - // Store the compiled source and the original source hash, so we don't need to do the work again next time - ignoreObjectChange.add(name); // ignore the next change and don't restart scripts - adapter.extendForeignObject(name, { - common: { - sourceHash, - compiled, - }, - }); - } else { - adapter.log.error(`${name}: TypeScript compilation failed:\n${errors}`); - typeof callback === 'function' && callback(false, name); - return; } - } - const createdScript: JsScript | false = createVM(`${globalScript}\n${compiled}`, name, false); - if (!createdScript) { - if (typeof callback === 'function') { - callback(false, name); + const createdScript: JsScript | false = this.createVM(`${this.globalScript}\n${compiled}`, name, false); + if (!createdScript) { + return false; } - return; - } - context.scripts[name] = createdScript as JsScript; - execute(context.scripts[name], name, obj.common.engineType as ScriptType, obj.common.verbose, obj.common.debug); - if (typeof callback === 'function') { - callback(true, name); + this.scripts[name] = createdScript; + this.execute( + this.scripts[name], + name, + obj.common.engineType as ScriptType, + obj.common.verbose, + obj.common.debug, + ); + return true; } + + this.log.warn(`Unknown engine type for "${name}": ${obj.common.engineType}`); + return false; } - } else { + let _name: string; - if (obj && obj._id) { + if (obj?._id) { _name = obj._id; const scriptIdName = _name.substring(SCRIPT_CODE_MARKER.length); if (!scriptIdName.length || scriptIdName.endsWith('.')) { - adapter.log.error(`Script name ${_name} is invalid!`); - if (typeof callback === 'function') { - callback(false, _name); - } - return; + this.log.error(`Script name ${_name} is invalid!`); + return false; } - adapter.setState(`scriptEnabled.${scriptIdName}`, false, true); + await this.setState(`scriptEnabled.${scriptIdName}`, false, true); } - !obj && adapter.log.error('Invalid script'); - if (typeof callback === 'function') { - callback(false, _name); + if (!obj) { + this.log.error('Invalid script'); } + return false; + } + + async loadScriptById(id: string): Promise { + let obj: ioBroker.ScriptObject | null | undefined; + try { + obj = (await this.getForeignObjectAsync(id)) as ioBroker.ScriptObject | null | undefined; + } catch (err: any) { + this.log.error(`Invalid script "${id}": ${err}`); + } + if (!obj) { + return false; + } + return this.loadScript(obj); } -} -function load(nameOrObject: string | ioBroker.ScriptObject, callback?: (success: boolean, name: string) => void): void { - if (typeof nameOrObject === 'object') { + async loadScript(nameOrObject: ioBroker.ScriptObject): Promise { // create states for scripts - createActiveObject(nameOrObject._id, nameOrObject && nameOrObject.common && nameOrObject.common.enabled, () => - createProblemObject(nameOrObject._id, () => prepareScript(nameOrObject, callback)), - ); - } else { - adapter.getForeignObject(nameOrObject, (err, obj: ioBroker.ScriptObject): void => { - if (!obj || err) { - err && adapter.log.error(`Invalid script "${nameOrObject}": ${err}`); - typeof callback === 'function' && callback(false, nameOrObject as string); - } else { - return load(obj, callback); - } - }); + await this.createActiveObject(nameOrObject._id, nameOrObject?.common?.enabled); + await this.createProblemObject(nameOrObject._id); + return this.prepareScript(nameOrObject); + } + + getAstroEvent( + date: Date, + astroEvent: AstroEventName, + start: string, + end: string, + offsetMinutes: number | string, + isDayEnd: boolean, + latitude: number, + longitude: number, + useNextDay?: boolean, + ): Date { + let ts: Date = this.mods.suncalc.getTimes(date, latitude, longitude)[astroEvent]; + + if (!ts || ts.getTime().toString() === 'NaN') { + ts = isDayEnd ? getNextTimeEvent(end, useNextDay) : getNextTimeEvent(start, useNextDay); + } + ts.setMilliseconds(0); + ts.setMinutes(ts.getMinutes() + (parseInt(offsetMinutes as unknown as string, 10) || 0)); + + const [timeHoursStart, timeMinutesStart] = start.split(':'); + const nTimeHoursStart = parseInt(timeHoursStart, 10); + const nTimeMinutesStart = parseInt(timeMinutesStart, 10) || 0; + + if ( + ts.getHours() < nTimeHoursStart || + (ts.getHours() === nTimeHoursStart && ts.getMinutes() < nTimeMinutesStart) + ) { + ts = getNextTimeEvent(start, useNextDay); + ts.setSeconds(0); + } + + const [timeHoursEnd, timeMinutesEnd] = end.split(':'); + const nTimeHoursEnd = parseInt(timeHoursEnd, 10); + const nTimeMinutesEnd = parseInt(timeMinutesEnd, 10) || 0; + + if (ts.getHours() > nTimeHoursEnd || (ts.getHours() === nTimeHoursEnd && ts.getMinutes() > nTimeMinutesEnd)) { + ts = getNextTimeEvent(end, useNextDay); + ts.setSeconds(0); + } + + // if event in the past + if (date > ts && useNextDay) { + // take the next day + ts.setDate(ts.getDate() + 1); + } + return ts; } -} -function patternMatching(event, patternFunctions) { - let matched = false; - for (let i = 0, len = patternFunctions.length; i < len; i++) { - if (patternFunctions[i](event)) { - if (patternFunctions.logic === 'or') { - return true; + async timeSchedule(): Promise { + const now = new Date(); + let hours = now.getHours(); + const minutes = now.getMinutes(); + if (this.timeSettings.format12) { + if (hours > 12) { + hours -= 12; } - matched = true; - } else if (patternFunctions.logic === 'and') { - return false; } + let sHours: string; + if (this.timeSettings.leadingZeros) { + sHours = hours.toString().padStart(2, '0'); + } else { + sHours = hours.toString(); + } + + await this.setState('variables.dayTime', { + val: `${sHours}:${minutes.toString().padStart(2, '0')}`, + ack: true, + }); + + now.setMinutes(now.getMinutes() + 1); + now.setSeconds(0); + now.setMilliseconds(0); + const interval = now.getTime() - Date.now(); + this.timeScheduleTimer = setTimeout(() => this.timeSchedule(), interval); } - return matched; -} -async function getData(callback) { - await adapter.subscribeForeignObjectsAsync('*'); + async dayTimeSchedules(): Promise { + // get astrological event + if ( + this.config.latitude === undefined || + this.config.longitude === undefined || + (this.config.latitude as unknown as string) === '' || + (this.config.longitude as unknown as string) === '' || + this.config.latitude === null || + this.config.longitude === null + ) { + this.log.error('Longitude or latitude does not set. Cannot use astro.'); + return; + } - const adapterConfig: AdapterConfig = adapter.config as AdapterConfig; + // Calculate the next event today + const todayDate = getAstroStartOfDay(); + const nowDate = new Date(); - if (!adapterConfig.subscribe) { - await adapter.subscribeForeignStatesAsync('*'); - } else { - await adapter.subscribeStatesAsync('debug.to'); - await adapter.subscribeStatesAsync('scriptEnabled.*'); - } + const todaySunrise = this.getAstroEvent( + todayDate, + this.config.sunriseEvent, + this.config.sunriseLimitStart, + this.config.sunriseLimitEnd, + this.config.sunriseOffset, + false, + this.config.latitude, + this.config.longitude, + ); + const todaySunset = this.getAstroEvent( + todayDate, + this.config.sunsetEvent, + this.config.sunsetLimitStart, + this.config.sunsetLimitEnd, + this.config.sunsetOffset, + true, + this.config.latitude, + this.config.longitude, + ); - adapter.log.info('requesting all states'); + // Sunrise + let sunriseTimeout = todaySunrise.getTime() - nowDate.getTime(); + if (sunriseTimeout < 0 || sunriseTimeout > 3600000) { + sunriseTimeout = 3600000; + } - adapter.getForeignStates('*', (err, res) => { - if (!adapterConfig.subscribe) { - if (err || !res) { - adapter.log.error(`Could not initialize states: ${err ? err.message : 'no result'}`); - adapter.terminate(EXIT_CODES.START_IMMEDIATELY_AFTER_STOP); - return; - } - context.states = Object.assign(res, context.states); + // Sunset + let sunsetTimeout = todaySunset.getTime() - nowDate.getTime(); + if (sunsetTimeout < 0 || sunsetTimeout > 3600000) { + sunsetTimeout = 3600000; + } - addGetProperty(context.states); + const isDayTime: ioBroker.State | null | undefined = await this.getStateAsync('variables.isDayTime'); + let isDay: boolean; + if (sunriseTimeout < 5000) { + isDay = true; + } else if (sunsetTimeout < 5000) { + isDay = false; + } else { + // check if in between + isDay = nowDate.getTime() > todaySunrise.getTime() - 60000 && nowDate <= todaySunset; } - // remember all IDs - for (const id in res) { - if (Object.prototype.hasOwnProperty.call(res, id)) { - context.stateIds.push(id); - } + const valDayTime = isDayTime ? !!isDayTime.val : false; + if (valDayTime !== isDay || isDayTime === null) { + await this.setState('variables.isDayTime', isDay, true); } - statesInitDone = true; - adapter.log.info('received all states'); - objectsInitDone && typeof callback === 'function' && callback(); - }); - adapter.log.info('requesting all objects'); + const dayLightSaving: ioBroker.State | null | undefined = + await this.getStateAsync('variables.isDaylightSaving'); + const isDayLightSaving = dstOffsetAtDate(nowDate) !== 0; + const val = dayLightSaving ? !!dayLightSaving.val : false; - adapter.getObjectList({ include_docs: true }, (err, res) => { - if (err || !res || !res.rows) { - adapter.log.error(`Could not initialize objects: ${err ? err.message : 'no result'}`); - adapter.terminate(EXIT_CODES.START_IMMEDIATELY_AFTER_STOP); - return; + if (val !== isDayLightSaving || dayLightSaving === null) { + await this.setState('variables.isDaylightSaving', isDayLightSaving, true); } - context.objects = {}; - for (let i = 0; i < res.rows.length; i++) { - if (!res.rows[i].doc) { - adapter.log.debug(`Got empty object for index ${i} (${res.rows[i].id})`); - continue; - } - if (context.objects[res.rows[i].doc._id] === undefined) { - // If was already there ignore - context.objects[res.rows[i].doc._id] = res.rows[i].doc; - } - context.objects[res.rows[i].doc._id].type === 'enum' && context.enums.push(res.rows[i].doc._id); - // Collect all names - addToNames(context.objects[res.rows[i].doc._id]); + let nextTimeout = sunriseTimeout; + if (sunriseTimeout > sunsetTimeout) { + nextTimeout = sunsetTimeout; + } + nextTimeout = nextTimeout - 3000; + if (nextTimeout < 3000) { + nextTimeout = 3000; } - addGetProperty(context.objects); - const systemConfig = context.objects['system.config']; + this.dayScheduleTimer = setTimeout(() => this.dayTimeSchedules(), nextTimeout); + } - // set language for debug messages - if (systemConfig?.common?.language) { - setLanguage(systemConfig.common.language); - } else if (adapter.language) { - setLanguage(adapter.language); + stopTimeSchedules(): void { + if (this.dayScheduleTimer) { + clearTimeout(this.dayScheduleTimer); + this.dayScheduleTimer = null; } - context.language = getLanguage(); - - // try to use system coordinates - if (adapterConfig.useSystemGPS) { - if (systemConfig && systemConfig.common && systemConfig.common.latitude) { - adapterConfig.latitude = systemConfig.common.latitude; - adapterConfig.longitude = systemConfig.common.longitude; - } else if (adapter.latitude) { - adapterConfig.latitude = adapter.latitude; - adapterConfig.longitude = adapter.longitude; - } + if (this.sunScheduleTimer) { + clearTimeout(this.sunScheduleTimer); + this.sunScheduleTimer = null; } - adapterConfig.latitude = parseFloat(adapterConfig.latitude as unknown as string); - adapterConfig.longitude = parseFloat(adapterConfig.longitude as unknown as string); + if (this.timeScheduleTimer) { + clearTimeout(this.timeScheduleTimer); + this.timeScheduleTimer = null; + } + } - if (isNaN(adapterConfig.latitude)) { - adapter.log.warn(`Configured latitude is not a number - check (instance/system) configuration`); - } else if (adapterConfig.latitude < -90 || adapterConfig.latitude > 90) { - adapter.log.warn( - `Configured latitude "${adapterConfig.latitude}" is invalid - check (instance/system) configuration`, + async patchFont(): Promise { + let stat: Stats | undefined; + let dbFile: Buffer | undefined; + try { + stat = statSync(`${__dirname}/admin/vs/base/browser/ui/codicons/codicon/codicon.ttf`); + const _dbFile = await this.readFileAsync( + 'javascript.admin', + `vs/base/browser/ui/codicons/codicon/codicon.ttf`, ); + if (_dbFile?.file) { + dbFile = _dbFile.file as Buffer; + } + } catch { + // ignore } - if (isNaN(adapterConfig.longitude)) { - adapter.log.warn(`Configured longitude is not a number - check (instance/system) configuration`); - } else if (adapterConfig.longitude < -180 || adapterConfig.longitude > 180) { - adapter.log.warn( - `Configured longitude "${adapterConfig.longitude}" is invalid - check (instance/system) configuration`, - ); + if (stat?.size !== 73452 || dbFile?.byteLength !== 73452) { + try { + const buffer = Buffer.from( + JSON.parse(readFileSync(`${__dirname}/admin/vsFont/codicon.json`).toString()), + 'base64', + ); + + const jszip = await import('jszip'); + const zip = await jszip.loadAsync(buffer); + let data: ArrayBuffer | undefined; + if (zip) { + data = await zip.file('codicon.ttf')?.async('arraybuffer'); + if (data?.byteLength !== 73452) { + this.log.error(`Cannot patch font: invalid font file!`); + return false; + } + } else { + this.log.error(`Cannot patch font: invalid font file!`); + return false; + } + writeFileSync(`${__dirname}/admin/vs/base/browser/ui/codicons/codicon/codicon.ttf`, Buffer.from(data)); + // upload this file + await this.writeFileAsync( + 'javascript.admin', + 'vs/base/browser/ui/codicons/codicon/codicon.ttf', + Buffer.from(data), + ); + return true; + } catch (err: unknown) { + this.log.error(`Cannot patch font: ${err as Error}`); + return false; + } } + return false; + } - adapterConfig.sunriseEvent = adapterConfig.sunriseEvent || 'nightEnd'; - adapterConfig.sunriseOffset = adapterConfig.sunriseOffset || 0; - adapterConfig.sunriseLimitStart = adapterConfig.sunriseLimitStart || '06:00'; - adapterConfig.sunriseLimitEnd = adapterConfig.sunriseLimitEnd || '12:00'; + async sunTimeSchedules(): Promise { + if (this.config.createAstroStates) { + if (!isNaN(this.config.longitude) && !isNaN(this.config.longitude)) { + const calcDate = getAstroStartOfDay(); - adapterConfig.sunsetEvent = adapterConfig.sunsetEvent || 'dusk'; - adapterConfig.sunsetOffset = adapterConfig.sunsetOffset || 0; - adapterConfig.sunsetLimitStart = adapterConfig.sunsetLimitStart || '18:00'; - adapterConfig.sunsetLimitEnd = adapterConfig.sunsetLimitEnd || '23:00'; + const times = this.mods.suncalc.getTimes(calcDate, this.config.latitude, this.config.longitude); - objectsInitDone = true; - adapter.log.info('received all objects'); - statesInitDone && typeof callback === 'function' && callback(); - }); -} + this.log.debug(`[sunTimeSchedules] Times: ${JSON.stringify(times)}`); -const debugState: { - scriptName: string; - child: null | ChildProcess; - promiseOnEnd: null | Promise; - paused: boolean; - started: number; - endTimeout?: null | NodeJS.Timeout; - running: boolean; - adapterInstance?: string; - breakOnStart?: boolean; -} = { - scriptName: '', - child: null, - promiseOnEnd: null, - paused: false, - started: 0, - running: false, -}; + for (const t in times) { + try { + const objId = `variables.astro.${t}`; + + await this.setObjectNotExistsAsync(objId, { + type: 'state', + common: { + name: `Astro ${t}`, + type: 'string', + role: 'value', + read: true, + write: false, + }, + native: {}, + }); -function debugStop(): Promise { - if (debugState.child) { - debugSendToInspector({ cmd: 'end' }); - debugState.endTimeout = setTimeout(() => { - debugState.endTimeout = null; - debugState.child?.kill('SIGTERM'); - }, 500); - } else { - debugState.promiseOnEnd = Promise.resolve(); - } + if (times[t] !== null && !isNaN(times[t].getTime())) { + const timeFormatted = formatHoursMinutesSeconds(times[t]); + await this.setState(objId, { + val: timeFormatted, + c: times[t].toISOString(), + ack: true, + }); + } else { + await this.setState(objId, { val: null, c: 'n/a', ack: true, q: 0x01 }); + } + } catch (err: unknown) { + this.log.error( + `[sunTimeSchedules] Unable to set state for astro time "${t}" (${times[t].getTime()}): ${err as Error}`, + ); + } + } - return debugState.promiseOnEnd.then(() => { - debugState.child = null; - debugState.running = false; - debugState.scriptName = ''; - debugState.endTimeout && clearTimeout(debugState.endTimeout); - debugState.endTimeout = null; - }); -} + const todayDate = new Date(); + todayDate.setHours(0); + todayDate.setMinutes(0); + todayDate.setSeconds(1); + todayDate.setMilliseconds(0); + todayDate.setDate(todayDate.getDate() + 1); -async function debugDisableScript(id: string | undefined): Promise { - if (id) { - const obj = context.objects[id]; - if (obj?.common?.enabled) { - await adapter.extendForeignObjectAsync(obj._id, { common: { enabled: false } }); + this.log.debug(`[sunTimeSchedules] Next: ${todayDate.toISOString()}`); + this.sunScheduleTimer = setTimeout(() => this.sunTimeSchedules(), todayDate.getTime() - Date.now()); + } + } else { + // remove astro states if disabled + this.delObject('variables.astro', { recursive: true }); } } -} -function debugSendToInspector(message: any): void { - if (debugState.child) { + /** + * Redirects the virtual-tsc log output to the ioBroker log + */ + tsLog = (message: string, severity?: ioBroker.LogLevel): void => { + // shift the severities around, we don't care about the small details + if (!severity || severity === 'info') { + severity = 'debug'; + } else if (severity === 'debug') { + // Don't spam build logs on Travis + if (isCI) { + return; + } + severity = 'silly'; + } + + if (this?.log) { + this.log[severity](message); + } else { + console.log(`[${severity.toUpperCase()}] ${message}`); + } + }; + + addGetProperty(object: Record): void { try { - adapter.log.info(`send to debugger: ${message}`); - debugState.child.send(message); - } catch (e) { - debugStop().then(() => - adapter.log.info(`Debugging of "${debugState.scriptName}" was stopped, because started in normal mode`), - ); + Object.defineProperty(object, 'get', { + value: function (id: string): any { + return this[id] || this[`${this.namespace}.${id}`]; + }, + enumerable: false, + }); + } catch { + console.error('Cannot install get property'); + } + } + + /** + * @param scriptID - The current script the declarations were generated from + * @param declarations + */ + provideDeclarationsForGlobalScript(scriptID: string, declarations: string): void { + // Remember which declarations this global script had access to, + // we need this so the editor doesn't show a duplicate identifier error + if (this.globalDeclarations != null && this.globalDeclarations !== '') { + this.knownGlobalDeclarationsByScript[scriptID] = this.globalDeclarations; + } + // and concatenate the global declarations for the next scripts + this.globalDeclarations += `${declarations}\n`; + // remember all previously generated global declarations, + // so global scripts can reference each other + const globalDeclarationPath = 'global.d.ts'; + tsAmbient[globalDeclarationPath] = this.globalDeclarations; + // make sure the next script compilation has access to the updated declarations + this.tsServer.provideAmbientDeclarations({ + [globalDeclarationPath]: this.globalDeclarations, + }); + jsDeclarationServer.provideAmbientDeclarations({ + [globalDeclarationPath]: this.globalDeclarations, + }); + } + + fixLineNo(line: string): string { + if (line.includes('javascript.js:')) { + return line; } - } else { - adapter.log.error(`Cannot send command to terminated inspector`); - adapter.setState( - 'debug.from', - JSON.stringify({ cmd: 'error', error: `Cannot send command to terminated inspector`, id: 1 }), - true, - ); + if (!/script[s]?\.js[.\\/]/.test(line)) { + return line; + } + if (/:([\d]+):/.test(line)) { + line = line.replace( + /:([\d]+):/, + ($0, $1) => `:${$1 > this.globalScriptLines + 1 ? $1 - this.globalScriptLines - 1 : $1}:`, + ); // one line for 'async function ()' + } else { + line = line.replace( + /:([\d]+)$/, + ($0, $1) => `:${$1 > this.globalScriptLines + 1 ? $1 - this.globalScriptLines - 1 : $1}`, + ); // one line for 'async function ()' + } + return line; + } + + debugStop(): Promise { + if (this.debugState.child) { + this.debugSendToInspector({ cmd: 'end' }); + this.debugState.endTimeout = setTimeout(() => { + this.debugState.endTimeout = null; + this.debugState.child?.kill('SIGTERM'); + }, 500); + this.debugState.promiseOnEnd = this.debugState.promiseOnEnd || Promise.resolve(0); + } else { + this.debugState.promiseOnEnd = Promise.resolve(0); + } + + return this.debugState.promiseOnEnd.then(() => { + this.debugState.child = null; + this.debugState.running = false; + this.debugState.scriptName = ''; + if (this.debugState.endTimeout) { + clearTimeout(this.debugState.endTimeout); + this.debugState.endTimeout = null; + } + }); } -} -function debugStart(data: { - breakOnStart?: boolean; - scriptName?: string; - adapter?: string -}): void { - if (Date.now() - debugState.started < 1000) { - console.log('Start ignored'); - return; + async debugDisableScript(id: string | undefined): Promise { + if (id) { + const obj = this.objects[id]; + if (obj?.common?.enabled) { + await this.extendForeignObjectAsync(obj._id, { common: { enabled: false } }); + } + } } - debugState.started = Date.now(); - // stop the script if it's running - debugDisableScript(data.scriptName) - .then(() => debugStop()) - .then(() => { - if (data.adapter) { - debugState.adapterInstance = data.adapter; - debugState.scriptName = ''; - } else { - debugState.adapterInstance = ''; - debugState.scriptName = data.scriptName; + debugSendToInspector(message: any): void { + if (this.debugState.child) { + try { + this.log.info(`send to debugger: ${message}`); + this.debugState.child.send(message); + } catch { + this.debugStop().then(() => + this.log.info( + `Debugging of "${this.debugState.scriptName}" was stopped, because started in normal mode`, + ), + ); } + } else { + this.log.error(`Cannot send command to terminated inspector`); + this.setState( + 'debug.from', + JSON.stringify({ cmd: 'error', error: `Cannot send command to terminated inspector`, id: 1 }), + true, + ); + } + } - debugState.breakOnStart = data.breakOnStart; + debugStart(data: { breakOnStart?: boolean; scriptName?: string; adapter?: string }): void { + if (Date.now() - this.debugState.started < 1000) { + console.log('Start ignored'); + return; + } - debugState.promiseOnEnd = new Promise(resolve => { - const options: ForkOptions = { - stdio: ['ignore', 'inherit', 'inherit', 'ipc'], - //stdio: ['pipe', 'pipe', 'pipe', 'ipc'] - }; - const args: string[] = []; - if (debugState.adapterInstance) { - args.push('--breakOnStart'); - } - - debugState.child = fork(`${__dirname}/lib/inspect.ts`, args, options); - - /*debugState.child.stdout.setEncoding('utf8'); - debugState.child.stderr.setEncoding('utf8'); - debugState.child.stdout.on('data', childPrint); - debugState.child.stderr.on('data', childPrint);*/ - - debugState.child?.on('message', (message: string | { - cmd: 'ready' | 'watched' | 'paused' | 'resumed' | 'log' | 'readyToDebug'; - severity?: string; - text?: string; - scriptId?: string; - script?: string; - }) => { - let oMessage: { - cmd: 'ready' | 'watched' | 'paused' | 'resumed' | 'log' | 'readyToDebug'; - severity?: string; - text?: string; - scriptId?: string; - script?: string; + this.debugState.started = Date.now(); + // stop the script if it's running + this.debugDisableScript(data.scriptName) + .then(() => this.debugStop()) + .then(() => { + if (data.adapter) { + this.debugState.adapterInstance = data.adapter; + this.debugState.scriptName = ''; + } else { + this.debugState.adapterInstance = ''; + this.debugState.scriptName = data.scriptName as string; + } + + this.debugState.breakOnStart = data.breakOnStart; + + this.debugState.promiseOnEnd = new Promise(resolve => { + const options: ForkOptions = { + stdio: ['ignore', 'inherit', 'inherit', 'ipc'], + //stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }; - if (typeof message === 'string') { - try { - oMessage = JSON.parse(message); - } catch (e) { - return adapter.log.error(`Cannot parse message from inspector: ${message}`); - } - } else { - oMessage = message; + const args: string[] = []; + if (this.debugState.adapterInstance) { + args.push('--breakOnStart'); } - if (oMessage.cmd !== 'ready') { - adapter.setState('debug.from', JSON.stringify(oMessage), true); - } + this.debugState.child = fork(`${__dirname}/lib/inspect.ts`, args, options); + + /*debugState.child.stdout.setEncoding('utf8'); + debugState.child.stderr.setEncoding('utf8'); + debugState.child.stdout.on('data', childPrint); + debugState.child.stderr.on('data', childPrint);*/ + + this.debugState.child?.on( + 'message', + ( + message: + | string + | { + cmd: 'ready' | 'watched' | 'paused' | 'resumed' | 'log' | 'readyToDebug'; + severity?: string; + text?: string; + scriptId?: string; + script?: string; + }, + ) => { + let oMessage: { + cmd: 'ready' | 'watched' | 'paused' | 'resumed' | 'log' | 'readyToDebug'; + severity?: string; + text?: string; + scriptId?: string; + script?: string; + }; + if (typeof message === 'string') { + try { + oMessage = JSON.parse(message); + } catch { + return this.log.error(`Cannot parse message from inspector: ${message}`); + } + } else { + oMessage = message; + } - switch (oMessage.cmd) { - case 'ready': { - debugSendToInspector({ - cmd: 'start', - scriptName: debugState.scriptName, - adapterInstance: debugState.adapterInstance, - instance: adapter.instance, - }); - break; - } + if (oMessage.cmd !== 'ready') { + this.setState('debug.from', JSON.stringify(oMessage), true); + } - case 'watched': { - //console.log(`WATCHED: ${JSON.stringify(oMessage)}`); - break; - } + switch (oMessage.cmd) { + case 'ready': { + this.debugSendToInspector({ + cmd: 'start', + scriptName: this.debugState.scriptName, + adapterInstance: this.debugState.adapterInstance, + instance: this.instance, + }); + break; + } - case 'paused': { - debugState.paused = true; - console.log(`host: PAUSED`); - break; - } + case 'watched': { + //console.log(`WATCHED: ${JSON.stringify(oMessage)}`); + break; + } - case 'resumed': { - debugState.paused = false; - //console.log(`STARTED`); - break; - } + case 'paused': { + this.debugState.paused = true; + console.log(`host: PAUSED`); + break; + } - case 'log': { - console.log(`[${oMessage.severity}] ${oMessage.text}`); - break; - } + case 'resumed': { + this.debugState.paused = false; + //console.log(`STARTED`); + break; + } + + case 'log': { + console.log(`[${oMessage.severity}] ${oMessage.text}`); + break; + } + + case 'readyToDebug': { + console.log( + `host: readyToDebug (set breakpoints): [${oMessage.scriptId}] ${oMessage.script}`, + ); + break; + } + } + }, + ); + this.debugState.child?.on('error', error => { + this.log.error(`Cannot start inspector: ${error}`); + this.setState('debug.from', JSON.stringify({ cmd: 'error', error }), true); + }); - case 'readyToDebug': { - console.log( - `host: readyToDebug (set breakpoints): [${oMessage.scriptId}] ${oMessage.script}`, + this.debugState.child?.on('exit', (code: number): void => { + if (code) { + this.setState( + 'debug.from', + JSON.stringify({ cmd: 'error', error: `invalid response code: ${code}` }), + true, ); - break; } - } - }); - debugState.child?.on('error', error => { - adapter.log.error(`Cannot start inspector: ${error}`); - adapter.setState('debug.from', JSON.stringify({ cmd: 'error', error }), true); - }); - - debugState.child?.on('exit', (code: number): void => { - if (code) { - adapter.setState( - 'debug.from', - JSON.stringify({ cmd: 'error', error: `invalid response code: ${code}` }), - true, - ); - } - adapter.setState('debug.from', JSON.stringify({ cmd: 'debugStopped', code }), true); - debugState.child = null; - resolve(code); + this.setState('debug.from', JSON.stringify({ cmd: 'debugStopped', code }), true); + this.debugState.child = null; + resolve(code); + }); }); }); - }); -} - -async function patchFont(): Promise { - let stat: Stats | undefined; - let dbFile: Buffer | undefined; - try { - stat = statSync(`${__dirname}/admin/vs/base/browser/ui/codicons/codicon/codicon.ttf`); - const _dbFile = await adapter.readFileAsync( - 'javascript.admin', - `vs/base/browser/ui/codicons/codicon/codicon.ttf`, - ); - if (_dbFile?.file) { - dbFile = _dbFile.file as Buffer; - } - } catch { - // ignore } +} - if (stat?.size !== 73452 || dbFile?.byteLength !== 73452) { - try { - const buffer = Buffer.from( - JSON.parse(readFileSync(`${__dirname}/admin/vsFont/codicon.json`).toString()), - 'base64', - ); - - const jszip = await import('jszip'); - const zip = await jszip.loadAsync(buffer); - const data = await zip.file('codicon.ttf').async('arraybuffer'); - if (data.byteLength !== 73452) { - throw new Error('invalid font file!'); +function patternMatching( + event: EventObj, + patternFunctions: PatternEventCompareFunction[] & { logic?: 'and' | 'or' }, +): boolean { + let matched = false; + for (let i = 0, len = patternFunctions.length; i < len; i++) { + if (patternFunctions[i](event)) { + if (patternFunctions.logic === 'or') { + return true; } - writeFileSync(`${__dirname}/admin/vs/base/browser/ui/codicons/codicon/codicon.ttf`, Buffer.from(data)); - // upload this file - await adapter.writeFileAsync( - 'javascript.admin', - 'vs/base/browser/ui/codicons/codicon/codicon.ttf', - Buffer.from(data), - ); - return true; - } catch (error) { - adapter.log.error(`Cannot patch font: ${error}`); + matched = true; + } else if (patternFunctions.logic === 'and') { return false; } } - return false; + return matched; } // If started as allInOne mode => return function to create instance -if (module.parent) { - module.exports = startAdapter; +if (require.main !== module) { + // Export the constructor in compact mode + module.exports = (options: Partial | undefined) => new JavaScript(options); } else { - // or start the instance directly - startAdapter(); + // otherwise start the instance directly + (() => new JavaScript())(); } diff --git a/src/types.d.ts b/src/types.d.ts index f206c18c0..6328bda43 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,14 +1,14 @@ -import { Scheduler, type SchedulerRule } from './lib/scheduler'; -import { ExecOptions } from 'node:child_process'; -import { AxiosHeaderValue, ResponseType } from 'axios'; +import type { ExecOptions, ChildProcess } from 'node:child_process'; +import type { Script } from 'node:vm'; +import type { AxiosHeaderValue, ResponseType } from 'axios'; import type { Job } from 'node-schedule'; -import { EventObj } from './lib/eventObj'; + +import type { Scheduler, SchedulerRule } from './lib/scheduler'; +import type { EventObj } from './lib/eventObj'; import type { PatternEventCompareFunction } from './lib/patternCompareFunctions'; -import { AstroEvent } from './lib/consts'; -import * as JS from './lib/javascript'; -import {Script} from "node:vm"; +import type { AstroEvent } from './lib/consts'; -export interface AdapterConfig { +export interface JavaScriptAdapterConfig { latitude: number; longitude: number; enableSetObject: boolean; @@ -146,7 +146,7 @@ export type AstroRule = { export type SandboxType = { mods: Record; - _id: string; + _id: number; name: string; // deprecated scriptName: string; instance: number; @@ -167,7 +167,7 @@ export type SandboxType = { $: (selector: string) => any; log: (msg: string, severity?: ioBroker.LogLevel) => void; onLog: (severity: ioBroker.LogLevel, callback: (info: any) => void) => number; - onLogUnregister: (idOrCallbackOrSeverity: string | ((msg: string) => void)) => void; + onLogUnregister: (idOrCallbackOrSeverity: number | ((msg: string) => void)) => void; exec: ( cmd: string, options: ExecOptions | ((error: Error | null | string, stdout?: string, stderr?: string) => void), @@ -245,6 +245,7 @@ export type SandboxType = { | SchedulerRule | string | (TimeRule | AstroRule | Pattern | SchedulerRule | string)[], + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents callbackOrChangeTypeOrId: string | ChangeType | ((event?: EventObj) => void), value?: any, ) => @@ -269,6 +270,7 @@ export type SandboxType = { | SchedulerRule | string | (TimeRule | AstroRule | Pattern | SchedulerRule | string)[], + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents callbackOrChangeTypeOrId: string | ChangeType | ((event?: EventObj) => void), value?: any, ) => @@ -284,8 +286,14 @@ export type SandboxType = { fileNamePattern: string | string[], withFileOrCallback: | boolean - | ((id: string, fileName: string, size: number, file?: string | Buffer, mimeType?: string) => void), - callback?: (id: string, fileName: string, size: number, file?: string | Buffer, mimeType?: string) => void, + | ((id: string, fileName: string, size: number | null, file?: string | Buffer, mimeType?: string) => void), + callback?: ( + id: string, + fileName: string, + size: number | null, + file?: string | Buffer, + mimeType?: string, + ) => void, ) => undefined | FileSubscriptionResult | (undefined | FileSubscriptionResult)[]; offFile: ( idOrObject: FileSubscriptionResult | string | (FileSubscriptionResult | string)[], @@ -339,7 +347,7 @@ export type SandboxType = { getState: ( id: string, callback?: (err: Error | null | undefined, state?: ioBroker.State | null) => void, - ) => undefined | void | (ioBroker.State & { notExist?: true }); + ) => undefined | void | (ioBroker.State & { notExist?: true }) | null; existsState: ( id: string, callback?: (err: Error | null | undefined, stateExists?: boolean) => void, @@ -351,18 +359,18 @@ export type SandboxType = { getIdByName: (name: string, alwaysArray?: boolean) => string | string[] | null; getObject: ( id: string, - enumName: null | string | ((err: Error | null | undefined, obj?: ioBroker.Object | null | undefined) => void), - cb: (err: Error | null | undefined, obj?: ioBroker.Object | null | undefined) => void, + enumName: null | string | ((err: Error | null | undefined, obj?: ioBroker.Object | null) => void), + cb: (err: Error | null | undefined, obj?: ioBroker.Object | null) => void, ) => void; setObject: ( id: string, obj: ioBroker.Object, - callback?: (err?: Error | null | string | undefined, res?: { id: string }) => void, + callback?: (err?: Error | null | string, res?: { id: string }) => void, ) => void; extendObject: ( id: string, obj: Partial, - callback?: (err?: Error | null | string | undefined, res?: { id: string }) => void, + callback?: (err?: Error | null | string, res?: { id: string }) => void, ) => void; deleteObject: (id: string, isRecursive?: boolean, callback?: ioBroker.ErrorCallback) => void; getEnums: (enumName: string) => { id: string; members: string[]; name: ioBroker.StringOrTranslated }[]; @@ -376,13 +384,18 @@ export type SandboxType = { ) => void; createState: ( name: string, - initValue: ioBroker.StateValue, - forceCreation: boolean, - common: Partial, - native: Record, - callback: (err: Error | null) => void, + initValue: undefined | ioBroker.StateValue | ioBroker.State, + forceCreation: + | boolean + | undefined + | Record + | Partial + | ((err: Error | null) => void), + common?: Partial | ((err: Error | null) => void), + native?: Record | ((err: Error | null) => void), + callback?: (error: Error | null | undefined, id?: string) => void, ) => void; - deleteState: (id: string, callback: (err: Error | null) => void) => void; + deleteState: (id: string, callback: (err: Error | null | undefined, found?: boolean) => void) => void; sendTo: ( adapter: string, cmd: string, @@ -414,29 +427,33 @@ export type SandboxType = { ) => boolean; onStop: (cb: () => void, timeout?: number) => void; formatValue: (value: number | string, decimals: number | string, format?: string) => string; - formatDate: (date: Date, format: string, language: string) => string; + formatDate: ( + date: Date | string | number | iobJS.AstroDate, + format?: string, + language?: ioBroker.Languages, + ) => string; formatTimeDiff: (diff: number, format?: string) => string; getDateObject: (date: any) => Date; - writeFile: (adapter: string, fileName: string, data: any, callback: (err: Error | null) => void) => void; + writeFile: (adapter: string, fileName: string, data: any, callback?: (err?: Error | null) => void) => void; readFile: ( adapter: string, - fileName: string | ((err: Error | null, data?: Buffer | string, mimeType?: string) => void), - callback: (err: Error | null, data?: Buffer | string, mimeType?: string) => void, + fileName: string | ((err: Error | null | undefined, data?: Buffer | string, mimeType?: string) => void), + callback: (err: Error | null | undefined, data?: Buffer | string, mimeType?: string) => void, ) => void; - unlink: (adapter: string, fileName: string, callback: (err: Error | null) => void) => void; - delFile: (adapter: string, fileName: string, callback: (err: Error | null) => void) => void; - rename: (adapter: string, oldName: string, newName: string, callback: (err: Error | null) => void) => void; - renameFile: (adapter: string, oldName: string, newName: string, callback: (err: Error | null) => void) => void; + unlink: (adapter: string, fileName: string, callback?: (err?: Error | null) => void) => void; + delFile: (adapter: string, fileName: string, callback?: (err?: Error | null) => void) => void; + rename: (adapter: string, oldName: string, newName: string, callback?: (err?: Error | null) => void) => void; + renameFile: (adapter: string, oldName: string, newName: string, callback?: (err?: Error | null) => void) => void; getHistory: (instance: string, options: any, callback: (err: Error | null, result: any) => void) => void; - runScript: (scriptName: string, callback?: (err: Error | null) => void) => boolean; + runScript: (scriptName: string, callback?: (err?: Error | null) => void) => boolean; runScriptAsync: (scriptName: string) => Promise; startScript: ( scriptName: string, - ignoreIfStarted: boolean | ((err: Error | null, started: boolean) => void), - callback?: (err: Error | null, started: boolean) => void, + ignoreIfStarted: boolean | ((err: Error | null | undefined, started: boolean) => void), + callback?: (err: Error | null | undefined, started: boolean) => void, ) => boolean; startScriptAsync: (scriptName: string, ignoreIfStarted?: boolean) => Promise; - stopScript: (scriptName: string, callback: (err: Error | null, stopped: boolean) => void) => boolean; + stopScript: (scriptName: string, callback: (err: Error | null | undefined, stopped: boolean) => void) => boolean; stopScriptAsync: (scriptName: string) => Promise; isScriptActive: (scriptName: string) => boolean; startInstanceAsync: (instanceName: string) => Promise; @@ -469,11 +486,14 @@ export type SandboxType = { jsonataExpression: (data: any, expression: string) => any; wait: (ms: number) => Promise; sleep: (ms: number) => Promise; - onObject: (pattern: string, callback: (data: any) => void) => void; + onObject: ( + pattern: string | string[], + callback: (id: string, obj?: ioBroker.Object | null) => void, + ) => SubscribeObject | SubscribeObject[] | null; subscribeObject: ( pattern: string | string[], callback: (id: string, obj?: ioBroker.Object | null) => void, - ) => SubscribeObject | SubscribeObject[]; + ) => SubscribeObject | SubscribeObject[] | null; unsubscribeObject: (idOrObject: SubscribeObject | SubscribeObject[]) => boolean | boolean[]; _sendToFrontEnd: (blockId: string, data: any) => void; logHandler?: ioBroker.LogLevel | '*'; @@ -534,6 +554,18 @@ export type Pattern = { enumName?: RegExp | string | string[]; }; +export type DebugState = { + scriptName: string; + child: null | ChildProcess; + promiseOnEnd: null | Promise; + paused: boolean; + started: number; + endTimeout?: null | NodeJS.Timeout; + running: boolean; + adapterInstance?: string; + breakOnStart?: boolean; +}; + export type Selector = { attr: string; value: string; @@ -555,7 +587,7 @@ export type FileSubscriptionResult = { idRegEx: RegExp | undefined; fileRegEx: RegExp | undefined; withFile: boolean; - callback: (id: string, fileName: string, size: number, withFile: boolean) => void; + callback: (id: string, fileName: string, size: number | null, withFile: boolean) => void; }; export interface JavascriptContext { @@ -580,9 +612,9 @@ export interface JavascriptContext { adapterSubs: Record; cacheObjectEnums: Record; isEnums: boolean; // If some subscription wants enum - channels: Record; - devices: Record; - scheduler: Scheduler; + channels: Record | null; + devices: Record | null; + scheduler: Scheduler | null; timers: { [scriptName: string]: JavascriptTimer[] }; enums: string[]; timerId: number; @@ -597,29 +629,24 @@ export interface JavascriptContext { { sandbox: SandboxType; cb: (info: LogMessage) => void; - id: string; + id: number; severity: ioBroker.LogLevel | '*'; }[] >; tempDirectories: { [scriptName: string]: string }; // name: path folderCreationVerifiedObjects: Record; updateLogSubscriptions: () => void; - convertBackStringifiedValues: (id: string, state: ioBroker.State | null | undefined) => ioBroker.State; + convertBackStringifiedValues: ( + id: string, + state: ioBroker.State | null | undefined, + ) => ioBroker.State | null | undefined; updateObjectContext: (id: string, obj: ioBroker.Object) => void; - prepareStateObject: (id: string, state: ioBroker.StateValue | ioBroker.SettableState | null, ack?: boolean | 'true' | 'false') => ioBroker.State; + prepareStateObject: (id: string, state: ioBroker.SettableState | null) => ioBroker.State; debugMode: string | undefined; - timeSettings: { - format12: boolean; - leadingZeros: boolean; - }; rulesOpened: string | null; // opened rules - getAbsoluteDefaultDataDir: () => string; language: ioBroker.Languages; + getAbsoluteDefaultDataDir: () => string; logError: (message: string, ...args: any[]) => void; - logWithLineInfo?: { - warn: (message: string) => void; - error: (message: string) => void; - info: (message: string) => void; - }; + logWithLineInfo: (message: string) => void; schedules?: string[]; } diff --git a/test/lib/JS/testDeclarations.js b/test/lib/JS/testDeclarations.js index 7b42a2854..caa610b46 100644 --- a/test/lib/JS/testDeclarations.js +++ b/test/lib/JS/testDeclarations.js @@ -15,7 +15,7 @@ getState('id').ts; setState('id', 1); setState('id', 1, true); -setState('id', 1, (id) => { +setState('id', 1, id => { id && id.toLowerCase(); }); @@ -23,7 +23,7 @@ const selected = $('selector'); const test1 = selected.getState(); test1 && test1.val.toFixed(); -schedule({ astro: 'night' }, () => { }); +schedule({ astro: 'night' }, () => {}); // TODO: Add more tests diff --git a/test/lib/JS/tsconfig.json b/test/lib/JS/tsconfig.json index 95516ef08..4b25957a5 100644 --- a/test/lib/JS/tsconfig.json +++ b/test/lib/JS/tsconfig.json @@ -3,16 +3,13 @@ */ { - "files": [ - "../../../lib/javascript.d.ts", - "./testDeclarations.js", - ], - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "noEmit": true, - "target": "es6", - "moduleResolution": "node", - "skipLibCheck": true - } -} \ No newline at end of file + "files": ["../../../lib/javascript.d.ts", "./testDeclarations.js"], + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "target": "es6", + "moduleResolution": "node", + "skipLibCheck": true + } +} diff --git a/test/lib/TS/testDeclarations.ts b/test/lib/TS/testDeclarations.ts index 5fbcd2b50..97109d88b 100644 --- a/test/lib/TS/testDeclarations.ts +++ b/test/lib/TS/testDeclarations.ts @@ -3,34 +3,38 @@ strict mode in order to make use of control flow analysis */ -import { Equals, AssignableTo } from "alcalzone-shared/types"; +import { Equals, AssignableTo } from 'alcalzone-shared/types'; // Used to test the types -function assertTrue() { return undefined!; } -function assertFalse() { return undefined!; } +function assertTrue() { + return undefined!; +} +function assertFalse() { + return undefined!; +} // All these method invocations should be valid -getState("id"); -getState("id").ack; -getState("id").val; -getState("id").ts; - -setState("id", 1); -setState("id", 1, true); -setState("id", 1, (id) => { - id!.toLowerCase(); +getState('id'); +getState('id').ack; +getState('id').val; +getState('id').ts; + +setState('id', 1); +setState('id', 1, true); +setState('id', 1, id => { + id!.toLowerCase(); }); const selected = $('selector'); selected.getState()!.val.toFixed(); // Repro from #539 -$("*").setState(1); +$('*').setState(1); // Repro from #636 -$("*").each(async () => { }); +$('*').each(async () => {}); -schedule({ astro: "night" }, () => { }); +schedule({ astro: 'night' }, () => {}); // TODO: Add more tests @@ -39,32 +43,32 @@ schedule({ astro: "night" }, () => { }); // Repro from https://forum.iobroker.net/viewtopic.php?t=19990 -const state1 = getState("rollo_wz_sued"); +const state1 = getState('rollo_wz_sued'); if (state1.notExist) { - assertTrue>(); - assertFalse>>(); + assertTrue>(); + assertFalse>>(); } else if (state1.ack) { - assertTrue>>(); - assertFalse>(); + assertTrue>>(); + assertFalse>(); - let test1 = state1.val! * 100; - test1 += 100; + let test1 = state1.val! * 100; + test1 += 100; } onFile('vis.0', 'main/*', true, (id, fileName, size, data, mimeType) => { - assertTrue>(); - assertTrue>(); - assertTrue>(); - assertTrue>(); - assertTrue>(); + assertTrue>(); + assertTrue>(); + assertTrue>(); + assertTrue>(); + assertTrue>(); }); onFile('vis.0', 'main/*', false, (id, fileName, size, data, mimeType) => { - assertTrue>(); - assertTrue>(); + assertTrue>(); + assertTrue>(); }); onFile('vis.0', 'main/*', Math.random() > 0, (id, fileName, size, data, mimeType) => { - assertTrue>(); - assertTrue>(); -}); \ No newline at end of file + assertTrue>(); + assertTrue>(); +}); diff --git a/test/lib/TS/tsconfig.json b/test/lib/TS/tsconfig.json index ffd3c7e4a..59f511ba1 100644 --- a/test/lib/TS/tsconfig.json +++ b/test/lib/TS/tsconfig.json @@ -3,17 +3,14 @@ */ { - "files": [ - "../../../lib/javascript.d.ts", - "./testDeclarations.ts", - ], - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "noEmit": true, - "target": "es6", - "moduleResolution": "node", - "skipLibCheck": true, - "strict": true - } -} \ No newline at end of file + "files": ["../../../lib/javascript.d.ts", "./testDeclarations.ts"], + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "target": "es6", + "moduleResolution": "node", + "skipLibCheck": true, + "strict": true + } +} diff --git a/test/lib/setup.js b/test/lib/setup.js index a73569f67..76428aa24 100644 --- a/test/lib/setup.js +++ b/test/lib/setup.js @@ -1,12 +1,12 @@ -/* jshint -W097 */// jshint strict:false +/* jshint -W097 */ // jshint strict:false /*jslint node: true */ // check if tmp directory exists -const fs = require('node:fs'); -const path = require('node:path'); +const fs = require('node:fs'); +const path = require('node:path'); const child_process = require('node:child_process'); -const rootDir = path.normalize(__dirname + '/../../'); -const pkg = require(rootDir + 'package.json'); -const debug = typeof v8debug === 'object'; +const rootDir = path.normalize(`${__dirname}/../../`); +const pkg = require(`${rootDir}package.json`); +const debug = typeof v8debug === 'object'; pkg.main = pkg.main || 'main.js'; let JSONLDB; @@ -23,14 +23,14 @@ function getAppName() { function loadJSONLDB() { if (!JSONLDB) { const dbPath = require.resolve('@alcalzone/jsonl-db', { - paths: [rootDir + 'tmp/node_modules', rootDir, rootDir + 'tmp/node_modules/' + appName + '.js-controller'] + paths: [rootDir + 'tmp/node_modules', rootDir, `${rootDir}tmp/node_modules/${appName}.js-controller`], }); - console.log('JSONLDB path: ' + dbPath); + console.log(`JSONLDB path: ${dbPath}`); try { const { JsonlDB } = require(dbPath); JSONLDB = JsonlDB; } catch (err) { - console.log('Jsonl require error: ' + err); + console.log(`Jsonl require error: ${err}`); } } } @@ -45,21 +45,19 @@ let pid = null; let systemConfig = null; function copyFileSync(source, target) { - let targetFile = target; //if target is a directory a new file with the same name will be created if (fs.existsSync(target)) { - if ( fs.lstatSync( target ).isDirectory() ) { + if (fs.lstatSync(target).isDirectory()) { targetFile = path.join(target, path.basename(source)); } } try { fs.writeFileSync(targetFile, fs.readFileSync(source)); - } - catch (err) { - console.log('file copy error: ' +source +' -> ' + targetFile + ' (error ignored)'); + } catch (err) { + console.log(`file copy error: ${source} -> ${targetFile} (error ignored)`); } } @@ -88,9 +86,15 @@ function copyFolderRecursiveSync(source, target, ignore) { const curTarget = path.join(targetFolder, file); if (fs.lstatSync(curSource).isDirectory()) { // ignore grunt files - if (file.indexOf('grunt') !== -1) return; - if (file === 'chai') return; - if (file === 'mocha') return; + if (file.indexOf('grunt') !== -1) { + return; + } + if (file === 'chai') { + return; + } + if (file === 'mocha') { + return; + } copyFolderRecursiveSync(curSource, targetFolder, ignore); } else { copyFileSync(curSource, curTarget); @@ -99,16 +103,16 @@ function copyFolderRecursiveSync(source, target, ignore) { } } -if (!fs.existsSync(rootDir + 'tmp')) { - fs.mkdirSync(rootDir + 'tmp'); +if (!fs.existsSync(`${rootDir}tmp`)) { + fs.mkdirSync(`${rootDir}tmp`); } async function storeOriginalFiles() { console.log('Store original files...'); - const dataDir = rootDir + 'tmp/' + appName + '-data/'; + const dataDir = `${rootDir}tmp/${appName}-data/`; - if (fs.existsSync(dataDir + 'objects.json')) { - const f = fs.readFileSync(dataDir + 'objects.json'); + if (fs.existsSync(`${dataDir}objects.json`)) { + const f = fs.readFileSync(`${dataDir}objects.json`); const objects = JSON.parse(f.toString()); if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { objects['system.adapter.admin.0'].common.enabled = false; @@ -117,23 +121,23 @@ async function storeOriginalFiles() { objects['system.adapter.admin.1'].common.enabled = false; } - fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + fs.writeFileSync(`${dataDir}objects.json.original`, JSON.stringify(objects)); console.log('Store original objects.json'); } - if (fs.existsSync(dataDir + 'states.json')) { + if (fs.existsSync(`${dataDir}states.json`)) { try { - const f = fs.readFileSync(dataDir + 'states.json'); - fs.writeFileSync(dataDir + 'states.json.original', f); + const f = fs.readFileSync(`${dataDir}states.json`); + fs.writeFileSync(`${dataDir}states.json.original`, f); console.log('Store original states.json'); } catch (err) { console.log('no states.json found - ignore'); } } - if (fs.existsSync(dataDir + 'objects.jsonl')) { + if (fs.existsSync(`${dataDir}objects.jsonl`)) { loadJSONLDB(); - const db = new JSONLDB(dataDir + 'objects.jsonl'); + const db = new JSONLDB(`${dataDir}objects.jsonl`); await db.open(); const admin0 = db.get('system.adapter.admin.0'); @@ -153,21 +157,21 @@ async function storeOriginalFiles() { } await db.close(); - const f = fs.readFileSync(dataDir + 'objects.jsonl'); - fs.writeFileSync(dataDir + 'objects.jsonl.original', f); + const f = fs.readFileSync(`${dataDir}objects.jsonl`); + fs.writeFileSync(`${dataDir}objects.jsonl.original`, f); console.log('Store original objects.jsonl'); } - if (fs.existsSync(dataDir + 'states.jsonl')) { - const f = fs.readFileSync(dataDir + 'states.jsonl'); - fs.writeFileSync(dataDir + 'states.jsonl.original', f); + if (fs.existsSync(`${dataDir}states.jsonl`)) { + const f = fs.readFileSync(`${dataDir}states.jsonl`); + fs.writeFileSync(`${dataDir}states.jsonl.original`, f); console.log('Store original states.jsonl'); } } function restoreOriginalFiles() { console.log('restoreOriginalFiles...'); - const dataDir = rootDir + 'tmp/' + appName + '-data/'; + const dataDir = `${rootDir}tmp/${appName}-data/`; if (fs.existsSync(dataDir + 'objects.json.original')) { const f = fs.readFileSync(dataDir + 'objects.json.original'); @@ -234,7 +238,6 @@ async function checkIsAdapterInstalled(cb, counter, customName) { } else { console.error('checkIsAdapterInstalled: No objects file found in datadir ' + dataDir); } - } catch (err) { console.log('checkIsAdapterInstalled: catch ' + err); } @@ -244,7 +247,7 @@ async function checkIsAdapterInstalled(cb, counter, customName) { if (cb) cb('Cannot install'); } else { console.log('checkIsAdapterInstalled: wait...'); - setTimeout(function() { + setTimeout(function () { checkIsAdapterInstalled(cb, counter + 1); }, 1000); } @@ -288,20 +291,17 @@ async function checkIsControllerInstalled(cb, counter) { }, 100); return; } - } else { console.error('checkIsControllerInstalled: No objects file found in datadir ' + dataDir); } - } catch (err) { - - } + } catch (err) {} if (counter > 20) { console.log('checkIsControllerInstalled: Cannot install!'); if (cb) cb('Cannot install'); } else { console.log('checkIsControllerInstalled: wait...'); - setTimeout(function() { + setTimeout(function () { checkIsControllerInstalled(cb, counter + 1); }, 1000); } @@ -318,8 +318,8 @@ function installAdapter(customName, cb) { // make first install if (debug) { child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { - cwd: rootDir + 'tmp', - stdio: [0, 1, 2] + cwd: rootDir + 'tmp', + stdio: [0, 1, 2], }); checkIsAdapterInstalled(function (error) { if (error) console.error(error); @@ -329,8 +329,8 @@ function installAdapter(customName, cb) { } else { // add controller const _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { - cwd: rootDir + 'tmp', - stdio: [0, 1, 2, 'ipc'] + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'], }); waitForEnd(_pid, function () { @@ -364,12 +364,20 @@ function waitForEnd(_pid, cb) { function installJsController(cb) { console.log('installJsController...'); - if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || - !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + if ( + !fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data') + ) { // try to detect appName.js-controller in node_modules/appName.js-controller // travis CI installs js-controller into node_modules if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { - console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + console.log( + 'installJsController: no js-controller => copy it from "' + + rootDir + + 'node_modules/' + + appName + + '.js-controller"', + ); // copy all // stop controller console.log('Stop controller if running...'); @@ -378,12 +386,12 @@ function installJsController(cb) { // start controller _pid = child_process.exec('node ' + appName + '.js stop', { cwd: rootDir + 'node_modules/' + appName + '.js-controller', - stdio: [0, 1, 2] + stdio: [0, 1, 2], }); } else { _pid = child_process.fork(appName + '.js', ['stop'], { - cwd: rootDir + 'node_modules/' + appName + '.js-controller', - stdio: [0, 1, 2, 'ipc'] + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'], }); } @@ -392,9 +400,12 @@ function installJsController(cb) { if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); - if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { console.log('Copy js-controller...'); - copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + copyFolderRecursiveSync( + rootDir + 'node_modules/' + appName + '.js-controller', + rootDir + 'tmp/node_modules/', + ); } console.log('Setup js-controller...'); @@ -403,12 +414,12 @@ function installJsController(cb) { // start controller _pid = child_process.exec('node ' + appName + '.js setup first --console', { cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', - stdio: [0, 1, 2] + stdio: [0, 1, 2], }); } else { __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { - cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', - stdio: [0, 1, 2, 'ipc'] + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'], }); } waitForEnd(__pid, function () { @@ -416,12 +427,15 @@ function installJsController(cb) { // change ports for object and state DBs const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); config.objects.port = 19001; - config.states.port = 19000; + config.states.port = 19000; // TEST WISE! //config.objects.type = 'jsonl'; //config.states.type = 'jsonl'; - fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + fs.writeFileSync( + rootDir + 'tmp/' + appName + '-data/' + appName + '.json', + JSON.stringify(config, null, 2), + ); console.log('Setup finished.'); copyAdapterToController(); @@ -437,8 +451,10 @@ function installJsController(cb) { // check if port 9000 is free, else admin adapter will be added to running instance const client = new require('net').Socket(); client.on('error', () => {}); - client.connect(9000, '127.0.0.1', function() { - console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + client.connect(9000, '127.0.0.1', function () { + console.error( + 'Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.', + ); process.exit(0); }); @@ -448,8 +464,8 @@ function installJsController(cb) { console.log('installJsController: no js-controller => install dev build from npm'); child_process.execSync('npm install ' + appName + '.js-controller@latest --prefix ./ --omit=dev', { - cwd: rootDir + 'tmp/', - stdio: [0, 1, 2] + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2], }); } else { console.log('Setup js-controller...'); @@ -457,12 +473,12 @@ function installJsController(cb) { // start controller child_process.exec('node ' + appName + '.js setup first', { cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', - stdio: [0, 1, 2] + stdio: [0, 1, 2], }); } else { child_process.fork(appName + '.js', ['setup', 'first'], { - cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', - stdio: [0, 1, 2, 'ipc'] + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'], }); } } @@ -473,8 +489,8 @@ function installJsController(cb) { if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { _pid = child_process.fork(appName + '.js', ['stop'], { - cwd: rootDir + 'node_modules/' + appName + '.js-controller', - stdio: [0, 1, 2, 'ipc'] + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'], }); } @@ -482,12 +498,15 @@ function installJsController(cb) { // change ports for object and state DBs const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); config.objects.port = 19001; - config.states.port = 19000; + config.states.port = 19000; // TEST WISE! //config.objects.type = 'jsonl'; //config.states.type = 'jsonl'; - fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + fs.writeFileSync( + rootDir + 'tmp/' + appName + '-data/' + appName + '.json', + JSON.stringify(config, null, 2), + ); copyAdapterToController(); @@ -510,7 +529,13 @@ function installJsController(cb) { function copyAdapterToController() { console.log('Copy adapter...'); // Copy adapter to tmp/node_modules/appName.adapter - copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', [ + '.idea', + 'test', + 'tmp', + '.git', + appName + '.js-controller', + ]); console.log('Adapter copied.'); } @@ -526,7 +551,7 @@ function clearControllerLog() { files = []; fs.mkdirSync(dirPath); } - } catch(e) { + } catch (e) { console.error('Cannot read "' + dirPath + '"'); return; } @@ -555,7 +580,7 @@ function clearDB() { files = []; fs.mkdirSync(dirPath); } - } catch(e) { + } catch (e) { console.error('Cannot read "' + dirPath + '"'); return; } @@ -592,10 +617,10 @@ function setupController(cb) { objs = JSON.parse(objs); } catch (e) { console.log('ERROR reading/parsing system configuration. Ignore'); - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } if (!objs || !objs['system.config']) { - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } systemConfig = objs['system.config']; @@ -631,13 +656,12 @@ async function getSecret() { try { objs = fs.readFileSync(dataDir + 'objects.json'); objs = JSON.parse(objs); - } - catch (e) { - console.warn("Could not load secret. Reason: " + e); + } catch (e) { + console.warn('Could not load secret. Reason: ' + e); return null; } if (!objs || !objs['system.config']) { - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } return objs['system.config'].native.secre; @@ -655,10 +679,9 @@ async function getSecret() { } else { console.error('read secret: No objects file found in datadir ' + dataDir); } - } -function encrypt (key, value) { +function encrypt(key, value) { let result = ''; for (let i = 0; i < value.length; ++i) { result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); @@ -680,13 +703,13 @@ function startAdapter(objects, states, callback) { // start controller pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { cwd: rootDir + 'tmp', - stdio: [0, 1, 2] + stdio: [0, 1, 2], }); } else { // start controller pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { - cwd: rootDir + 'tmp', - stdio: [0, 1, 2, 'ipc'] + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'], }); } } catch (error) { @@ -707,7 +730,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback } if (onStateChange === undefined) { - callback = onObjectChange; + callback = onObjectChange; onObjectChange = undefined; } @@ -724,19 +747,23 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback // rootDir + 'tmp/node_modules const objPath = require.resolve(`@iobroker/db-objects-${config.objects.type}`, { - paths: [ rootDir + 'tmp/node_modules', rootDir, rootDir + 'tmp/node_modules/' + appName + '.js-controller'] + paths: [ + rootDir + 'tmp/node_modules', + rootDir, + rootDir + 'tmp/node_modules/' + appName + '.js-controller', + ], }); console.log('Objects Path: ' + objPath); const Objects = require(objPath).Server; objects = new Objects({ connection: { - 'type': config.objects.type, - 'host': '127.0.0.1', - 'port': 19001, - 'user': '', - 'pass': '', - 'noFileCache': false, - 'connectTimeout': 2000 + type: config.objects.type, + host: '127.0.0.1', + port: 19001, + user: '', + pass: '', + noFileCache: false, + connectTimeout: 2000, }, logger: { silly: function (msg) { @@ -753,7 +780,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback }, error: function (msg) { console.error(msg); - } + }, }, connected: function () { isObjectConnected = true; @@ -769,12 +796,16 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback } } }, - change: onObjectChange + change: onObjectChange, }); // Just open in memory DB itself const statePath = require.resolve(`@iobroker/db-states-${config.states.type}`, { - paths: [ rootDir + 'tmp/node_modules', rootDir, rootDir + 'tmp/node_modules/' + appName + '.js-controller'] + paths: [ + rootDir + 'tmp/node_modules', + rootDir, + rootDir + 'tmp/node_modules/' + appName + '.js-controller', + ], }); console.log('States Path: ' + statePath); const States = require(statePath).Server; @@ -785,8 +816,8 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback port: 19000, options: { auth_pass: null, - retry_max_delay: 15000 - } + retry_max_delay: 15000, + }, }, logger: { silly: function (msg) { @@ -803,7 +834,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback }, error: function (msg) { console.log(msg); - } + }, }, connected: function () { isStatesConnected = true; @@ -819,7 +850,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback } } }, - change: onStateChange + change: onStateChange, }); } catch (err) { console.log(err); @@ -872,9 +903,9 @@ function stopController(cb) { if (objects) { console.log('Set system.adapter.' + pkg.name + '.0'); objects.setObject('system.adapter.' + pkg.name + '.0', { - common:{ - enabled: false - } + common: { + enabled: false, + }, }); } @@ -953,15 +984,15 @@ async function getAdapterConfig(instance) { if (typeof module !== undefined && module.parent) { module.exports.getAdapterConfig = getAdapterConfig; module.exports.setAdapterConfig = setAdapterConfig; - module.exports.startController = startController; - module.exports.stopController = stopController; - module.exports.setupController = setupController; - module.exports.stopAdapter = stopAdapter; - module.exports.startAdapter = startAdapter; - module.exports.installAdapter = installAdapter; - module.exports.appName = appName; - module.exports.adapterName = adapterName; - module.exports.adapterStarted = adapterStarted; - module.exports.getSecret = getSecret; - module.exports.encrypt = encrypt; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; + module.exports.getSecret = getSecret; + module.exports.encrypt = encrypt; } diff --git a/test/mocha.setup.js b/test/mocha.setup.js index 2adcb98ef..f64104034 100644 --- a/test/mocha.setup.js +++ b/test/mocha.setup.js @@ -1 +1,3 @@ -process.on("unhandledRejection", (r) => { throw r; }); +process.on('unhandledRejection', r => { + throw r; +}); diff --git a/test/testFunctions.js b/test/testFunctions.js index c594b0649..0b416280a 100644 --- a/test/testFunctions.js +++ b/test/testFunctions.js @@ -1,9 +1,9 @@ const expect = require('chai').expect; const { log } = require('node:console'); -const setup = require('./lib/setup'); +const setup = require('./lib/setup'); -let objects = null; -let states = null; +let objects = null; +let states = null; const stateChangedHandlers = new Set(); function onStateChanged(id, state) { @@ -89,11 +89,11 @@ describe.only('Test JS', function () { setup.setupController(async function () { const config = await setup.getAdapterConfig(); // enable adapter - config.common.enabled = true; + config.common.enabled = true; config.common.loglevel = 'debug'; config.native.longitude = 43.273709; - config.native.latitude = 6.5798918; + config.native.latitude = 6.5798918; await setup.setAdapterConfig(config.common, config.native); @@ -103,38 +103,39 @@ describe.only('Test JS', function () { (id, state) => onStateChanged && onStateChanged(id, state), (_objects, _states) => { objects = _objects; - states = _states; + states = _states; states.subscribe('*'); const script = { - _id: 'script.js.global.test_globalSetTestState', - type: 'script', + _id: 'script.js.global.test_globalSetTestState', + type: 'script', common: { - name: 'global function globalSetTestState', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `function globalSetTestState(val) {\n` + - ` createState('test_global_setTestState', () => {\n` + - ` setState('test_global_setTestState', val);\n` + - ` });\n` + - `}`, + name: 'global function globalSetTestState', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `function globalSetTestState(val) {\n` + + ` createState('test_global_setTestState', () => {\n` + + ` setState('test_global_setTestState', val);\n` + + ` });\n` + + `}`, }, - native: {} + native: {}, }; objects.setObject(script._id, script, err => { expect(err).to.be.null; setup.startAdapter(objects, states, () => _done()); }); - } + }, ); }); }); it('Test JS: Check if adapter started', function (done) { this.timeout(30000); - checkConnectionOfAdapter((err) => { + checkConnectionOfAdapter(err => { expect(err).to.be.null; done(); }); @@ -144,35 +145,36 @@ describe.only('Test JS', function () { this.timeout(10000); // add script const script = { - _id: 'script.js.test_compareTime', - type: 'script', + _id: 'script.js.test_compareTime', + type: 'script', common: { - name: 'compareTime', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('testCompareTime', -1, () => {\n` + - ` let count = 0;\n` + - ` count += compareTime('23:00', '01:00', 'between', '22:30') ? 0 : 1;\n` + - ` count += compareTime('23:00', '01:00', 'between', '02:30') ? 0 : 1;\n` + - ` count += compareTime('10:00', '20:00', 'between', '15:00') ? 1 : 0;\n` + - ` count += compareTime('10:00', '20:00', 'between', '9:00') ? 0 : 1;\n` + - ` count += compareTime('10:00', null, '<', '9:00') ? 1 : 0;\n` + - ` const date1 = new Date();\n` + - ` date1.setHours(10);\n` + - ` date1.setMinutes(0);\n` + - ` count += compareTime(date1, null, '<', '9:00') ? 1 : 0;\n` + - ` count += compareTime(date1, '20:00', 'between', '15:00') ? 1 : 0;\n` + - ` count += compareTime('5:00', date1, 'between', '8:00') ? 1 : 0;\n` + - ` const date2 = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);\n` + - ` date2.setHours(2);\n` + - ` date2.setMinutes(30);\n` + - ` count += compareTime('23:00', '01:00', 'between', date2) ? 0 : 1;\n` + - ` setState('testCompareTime', count, true);\n` + - `});`, + name: 'compareTime', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('testCompareTime', -1, () => {\n` + + ` let count = 0;\n` + + ` count += compareTime('23:00', '01:00', 'between', '22:30') ? 0 : 1;\n` + + ` count += compareTime('23:00', '01:00', 'between', '02:30') ? 0 : 1;\n` + + ` count += compareTime('10:00', '20:00', 'between', '15:00') ? 1 : 0;\n` + + ` count += compareTime('10:00', '20:00', 'between', '9:00') ? 0 : 1;\n` + + ` count += compareTime('10:00', null, '<', '9:00') ? 1 : 0;\n` + + ` const date1 = new Date();\n` + + ` date1.setHours(10);\n` + + ` date1.setMinutes(0);\n` + + ` count += compareTime(date1, null, '<', '9:00') ? 1 : 0;\n` + + ` count += compareTime(date1, '20:00', 'between', '15:00') ? 1 : 0;\n` + + ` count += compareTime('5:00', date1, 'between', '8:00') ? 1 : 0;\n` + + ` const date2 = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);\n` + + ` date2.setHours(2);\n` + + ` date2.setMinutes(30);\n` + + ` count += compareTime('23:00', '01:00', 'between', date2) ? 0 : 1;\n` + + ` setState('testCompareTime', count, true);\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { if (id === 'javascript.0.testCompareTime') { @@ -183,8 +185,7 @@ describe.only('Test JS', function () { expect(state.val).to.be.equal(9); done(); }); - } - else { + } else { console.log(`State testCompareTime.val = ${state.val}`); } } @@ -200,22 +201,23 @@ describe.only('Test JS', function () { this.timeout(10000); // add script const script = { - _id: 'script.js.test_httpGet_error', - type: 'script', + _id: 'script.js.test_httpGet_error', + type: 'script', common: { - name: 'test httpGet error', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_httpget_error', () => {\n` + - ` httpGet('http://google1456.com', (error, response) => {\n` + - ` if (error) {\n` + - ` console.error(error);\n` + - ` setState('test_httpget_error', true, true);\n` + - ` }\n` + - ` });\n` + - `});`, + name: 'test httpGet error', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_httpget_error', () => {\n` + + ` httpGet('http://google1456.com', (error, response) => {\n` + + ` if (error) {\n` + + ` console.error(error);\n` + + ` setState('test_httpget_error', true, true);\n` + + ` }\n` + + ` });\n` + + `});`, }, native: {}, }; @@ -236,15 +238,15 @@ describe.only('Test JS', function () { this.timeout(3000); // add script const script = { - _id: 'script.js.test_creation_of_state', - type: 'script', + _id: 'script.js.test_creation_of_state', + type: 'script', common: { - name: 'test creation of state', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_creation_of_state', 5);`, + name: 'test creation of state', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `createState('test_creation_of_state', 5);`, }, native: {}, }; @@ -273,15 +275,15 @@ describe.only('Test JS', function () { this.timeout(3000); // add script const script = { - _id: 'script.js.test_creation_of_foreign_state', - type: 'script', + _id: 'script.js.test_creation_of_foreign_state', + type: 'script', common: { - name: 'test creation of foreign state', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('javascript.1.test_creation_of_foreign_state', 6);`, + name: 'test creation of foreign state', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `createState('javascript.1.test_creation_of_foreign_state', 6);`, }, native: {}, }; @@ -310,17 +312,17 @@ describe.only('Test JS', function () { this.timeout(3000); // add script const script = { - _id: 'script.js.test_deletion_of_state', - type: 'script', + _id: 'script.js.test_deletion_of_state', + type: 'script', common: { - name: 'test deletion of state', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `deleteState('test_creation_of_state');`, + name: 'test deletion of state', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `deleteState('test_creation_of_state');`, }, - native: {} + native: {}, }; objects.getObject('javascript.0.test_creation_of_state', (err, obj) => { @@ -360,17 +362,17 @@ describe.only('Test JS', function () { this.timeout(3000); // add script const script = { - _id: 'script.js.test_deletion_of_foreign_state', - type: 'script', + _id: 'script.js.test_deletion_of_foreign_state', + type: 'script', common: { - name: 'test deletion of foreign state', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `deleteState('javascript.1.test_creation_of_foreign_state');`, + name: 'test deletion of foreign state', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `deleteState('javascript.1.test_creation_of_foreign_state');`, }, - native: {} + native: {}, }; objects.getObject('javascript.1.test_creation_of_foreign_state', (err, obj) => { @@ -406,22 +408,23 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_createState', - type: 'script', + _id: 'script.js.test_createState', + type: 'script', common: { - name: 'test createState', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `await createStateAsync('test_createState_init', 100);\n` + - `await createStateAsync('test_createState_common', { name: 'common', desc: 'test', type: 'array' });\n` + - `await createStateAsync('test_createState_initCommon', 101, { name: 'initCommon', desc: 'test', type: 'number' });\n` + - `await createStateAsync('test_createState_commonNative', { name: 'commonNative', desc: 'test', type: 'object' }, { customProperty: true });\n` + - `await createStateAsync('test_createState_initCommonNative', 102, { name: 'initCommonNative', desc: 'test', type: 'number' }, { customProperty: true });\n` + - `await createStateAsync('test_createState_initForce', true, true);\n` + - `await createStateAsync('test_createState_initForceCommon', false, true, { name: 'initFoceCommon', desc: 'test', type: 'boolean' });\n` + - `await createStateAsync('test_createState_initForceCommonNative', false, true, { name: 'initForceCommonNative', desc: 'test', type: 'boolean' }, { customProperty: true });\n`, + name: 'test createState', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `await createStateAsync('test_createState_init', 100);\n` + + `await createStateAsync('test_createState_common', { name: 'common', desc: 'test', type: 'array' });\n` + + `await createStateAsync('test_createState_initCommon', 101, { name: 'initCommon', desc: 'test', type: 'number' });\n` + + `await createStateAsync('test_createState_commonNative', { name: 'commonNative', desc: 'test', type: 'object' }, { customProperty: true });\n` + + `await createStateAsync('test_createState_initCommonNative', 102, { name: 'initCommonNative', desc: 'test', type: 'number' }, { customProperty: true });\n` + + `await createStateAsync('test_createState_initForce', true, true);\n` + + `await createStateAsync('test_createState_initForceCommon', false, true, { name: 'initFoceCommon', desc: 'test', type: 'boolean' });\n` + + `await createStateAsync('test_createState_initForceCommonNative', false, true, { name: 'initForceCommonNative', desc: 'test', type: 'boolean' }, { customProperty: true });\n`, }, native: {}, }; @@ -470,24 +473,35 @@ describe.only('Test JS', function () { expect(obj.common.type).to.be.equal('mixed'); expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); - objects.getObject('javascript.0.test_createState_initForceCommon', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('initFoceCommon'); - expect(obj.common.desc).to.be.equal('test'); - expect(obj.common.type).to.be.equal('boolean'); - expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); - - objects.getObject('javascript.0.test_createState_initForceCommonNative', (err, obj) => { + objects.getObject( + 'javascript.0.test_createState_initForceCommon', + (err, obj) => { expect(err).to.be.null; - expect(obj.common.name).to.be.equal('initForceCommonNative'); + expect(obj.common.name).to.be.equal('initFoceCommon'); expect(obj.common.desc).to.be.equal('test'); expect(obj.common.type).to.be.equal('boolean'); expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); - expect(obj.native).to.have.all.keys('customProperty'); - done(); - }); - }); + objects.getObject( + 'javascript.0.test_createState_initForceCommonNative', + (err, obj) => { + expect(err).to.be.null; + expect(obj.common.name).to.be.equal('initForceCommonNative'); + expect(obj.common.desc).to.be.equal('test'); + expect(obj.common.type).to.be.equal('boolean'); + expect(obj.native).to.not.have.any.keys( + 'name', + 'desc', + 'type', + 'role', + ); + expect(obj.native).to.have.all.keys('customProperty'); + + done(); + }, + ); + }, + ); }); }); }); @@ -502,22 +516,23 @@ describe.only('Test JS', function () { this.timeout(20000); // add script const script = { - _id: 'script.js.test_read_objects_db', - type: 'script', + _id: 'script.js.test_read_objects_db', + type: 'script', common: { - name: 'test read objects db', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `const fs = require('node:fs');\n` + - `try{\n` + - ` fs.readFileSync('${__dirname}/../tmp/${setup.appName}-data/objects.json');\n` + - `} catch (err) {\n` + - ` createState('test_read_objects_db', err.toString());\n` + - `}`, + name: 'test read objects db', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `const fs = require('node:fs');\n` + + `try{\n` + + ` fs.readFileSync('${__dirname}/../tmp/${setup.appName}-data/objects.json');\n` + + `} catch (err) {\n` + + ` createState('test_read_objects_db', err.toString());\n` + + `}`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { @@ -537,22 +552,23 @@ describe.only('Test JS', function () { this.timeout(3000); // add script const script = { - _id: 'script.js.test_write_objects_db', - type: 'script', + _id: 'script.js.test_write_objects_db', + type: 'script', common: { - name: 'test write objects db', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `const fs = require('node:fs');\n` + - `try{\n` + - ` fs.writeFileSync('${__dirname}/../tmp/${setup.appName}-data/objects.json');\n` + - `} catch (err) {\n` + - ` createState('test_write_objects_db', err.toString());\n` + - `}`, + name: 'test write objects db', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `const fs = require('node:fs');\n` + + `try{\n` + + ` fs.writeFileSync('${__dirname}/../tmp/${setup.appName}-data/objects.json');\n` + + `} catch (err) {\n` + + ` createState('test_write_objects_db', err.toString());\n` + + `}`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { if (id === 'javascript.0.test_write_objects_db' && state.val === 'Error: Permission denied') { @@ -570,24 +586,25 @@ describe.only('Test JS', function () { this.timeout(3000); // add script const script = { - _id: 'script.js.test_nodefs_write', - type: 'script', + _id: 'script.js.test_nodefs_write', + type: 'script', common: { - name: 'test node:fs write to files', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `const fs = require('node:fs');\n` + - `try{\n` + - ` const filesPath = defaultDataDir + '/files/0_userdata.0/nodejswrite.txt';\n` + - ` log('Writing file to path: ' + filesPath);\n` + - ` fs.appendFile(filesPath, 'this is not allowed!');\n` + - `} catch (err) {\n` + - ` createState('test_nodefs_write', err.toString());\n` + - `}`, + name: 'test node:fs write to files', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `const fs = require('node:fs');\n` + + `try{\n` + + ` const filesPath = defaultDataDir + '/files/0_userdata.0/nodejswrite.txt';\n` + + ` log('Writing file to path: ' + filesPath);\n` + + ` fs.appendFile(filesPath, 'this is not allowed!');\n` + + `} catch (err) {\n` + + ` createState('test_nodefs_write', err.toString());\n` + + `}`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { if (id === 'javascript.0.test_nodefs_write' && state.val === 'Error: Permission denied') { @@ -605,27 +622,28 @@ describe.only('Test JS', function () { this.timeout(3000); // add script const script = { - _id: 'script.js.test_nodefs_read', - type: 'script', + _id: 'script.js.test_nodefs_read', + type: 'script', common: { - name: 'test node:fs read from files', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `const fs = require('node:fs');\n` + - `createState('test_nodefs_read', 'no', () => {\n` + - ` writeFile('0_userdata.0', 'nodejsread.txt', 'is allowed', (err) => {\n` + - ` if (!err) {\n` + - ` const filesPath = defaultDataDir + '/files/0_userdata.0/nodejsread.txt';\n` + - ` log('Read file from path: ' + filesPath);\n` + - ` const data = fs.readFileSync(filesPath);\n` + - ` setState('test_nodefs_read', { val: data.toString(), ack: true });\n` + - ` }\n` + - ` });\n` + - `});`, + name: 'test node:fs read from files', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `const fs = require('node:fs');\n` + + `createState('test_nodefs_read', 'no', () => {\n` + + ` writeFile('0_userdata.0', 'nodejsread.txt', 'is allowed', (err) => {\n` + + ` if (!err) {\n` + + ` const filesPath = defaultDataDir + '/files/0_userdata.0/nodejsread.txt';\n` + + ` log('Read file from path: ' + filesPath);\n` + + ` const data = fs.readFileSync(filesPath);\n` + + ` setState('test_nodefs_read', { val: data.toString(), ack: true });\n` + + ` }\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { if (id === 'javascript.0.test_nodefs_read' && state.val === 'is allowed') { @@ -648,22 +666,23 @@ describe.only('Test JS', function () { // add script const script = { - _id: 'script.js.test_open_objects_other_path', - type: 'script', + _id: 'script.js.test_open_objects_other_path', + type: 'script', common: { - name: 'test open objects other path', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `const fs = require('node:fs');\n` + - `try{\n` + - ` fs.writeFileSync('${__dirname.replace(/\\/g, '/')}/../tmp/objects.json', '${time}');\n` + - `} catch (err) {\n` + - ` createState('test_open_objects_other_path', err.toString());\n` + - `}`, + name: 'test open objects other path', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `const fs = require('node:fs');\n` + + `try{\n` + + ` fs.writeFileSync('${__dirname.replace(/\\/g, '/')}/../tmp/objects.json', '${time}');\n` + + `} catch (err) {\n` + + ` createState('test_open_objects_other_path', err.toString());\n` + + `}`, }, - native: {} + native: {}, }; objects.setObject(script._id, script, err => { @@ -691,20 +710,21 @@ describe.only('Test JS', function () { // add script const script = { - _id: 'script.js.test_createTempFile', - type: 'script', + _id: 'script.js.test_createTempFile', + type: 'script', common: { - name: 'test createTempFile', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_createTempFile', { type: 'string', read: true, write: false }, async () => {\n` + - ` const filePath = createTempFile('subdir/test.txt', 'CONTENT_OK');\n` + - ` await setStateAsync('test_createTempFile', { val: filePath, ack: true });\n` + - `});`, + name: 'test createTempFile', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_createTempFile', { type: 'string', read: true, write: false }, async () => {\n` + + ` const filePath = createTempFile('subdir/test.txt', 'CONTENT_OK');\n` + + ` await setStateAsync('test_createTempFile', { val: filePath, ack: true });\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { if (id === 'javascript.0.test_createTempFile' && state.val !== '-') { @@ -744,21 +764,21 @@ describe.only('Test JS', function () { 'nightEnd', 'nauticalDawn', 'dawn', - 'nadir' + 'nadir', ]; // add script const script = { - _id: 'script.js.test_getAstroDate', - type: 'script', + _id: 'script.js.test_getAstroDate', + type: 'script', common: { - name: 'test getAstroDate', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: '', + name: 'test getAstroDate', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: '', }, - native: {} + native: {}, }; for (let t = 0; t < types.length; t++) { script.common.source += `createState('test_getAstroDate_${types[t]}', getAstroDate('${types[t]}') ? getAstroDate('${types[t]}').toString() : '');\n`; @@ -768,7 +788,16 @@ describe.only('Test JS', function () { const onStateChanged = function (id, state) { if (types.includes(id.substring('javascript.0.test_getAstroDate_'.length))) { typesChanged[id] = true; - console.log('State change ' + id + ' / ' + Object.keys(typesChanged).length + '-' + types.length + ' = ' + JSON.stringify(state)); + console.log( + 'State change ' + + id + + ' / ' + + Object.keys(typesChanged).length + + '-' + + types.length + + ' = ' + + JSON.stringify(state), + ); if (Object.keys(typesChanged).length === types.length) { removeStateChangedHandler(onStateChanged); @@ -798,23 +827,24 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_setStateDelayed_simple', - type: 'script', + _id: 'script.js.test_setStateDelayed_simple', + type: 'script', common: { - name: 'test setStateDelayed simple', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_setStateDelayed_simple', 4, () => {\n` + - ` setStateDelayed('test_setStateDelayed_simple', 5, 1000);\n` + - `});`, + name: 'test setStateDelayed simple', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_setStateDelayed_simple', 4, () => {\n` + + ` setStateDelayed('test_setStateDelayed_simple', 5, 1000);\n` + + `});`, }, - native: {} + native: {}, }; let start = 0; - const onStateChanged = function (id, state){ + const onStateChanged = function (id, state) { if (id !== 'javascript.0.test_setStateDelayed_simple' || !state.val) return; if (state.val === 4) { start = state.ts; @@ -834,22 +864,23 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_setStateDelayed_stateObject', - type: 'script', + _id: 'script.js.test_setStateDelayed_stateObject', + type: 'script', common: { - name: 'test setStateDelayed stateObject', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_setStateDelayed_stateObject', false, { type: 'boolean', read: true, write: false }, async () => {\n` + - ` setStateDelayed('test_setStateDelayed_stateObject', { val: true, ack: true }, false, false);\n` + - `});`, + name: 'test setStateDelayed stateObject', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_setStateDelayed_stateObject', false, { type: 'boolean', read: true, write: false }, async () => {\n` + + ` setStateDelayed('test_setStateDelayed_stateObject', { val: true, ack: true }, false, false);\n` + + `});`, }, - native: {} + native: {}, }; - const onStateChanged = function (id, state){ + const onStateChanged = function (id, state) { if (id === 'javascript.0.test_setStateDelayed_stateObject') { expect(state.val).to.be.true; expect(state.ack).to.be.true; @@ -866,20 +897,21 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_setStateDelayed_nested', - type: 'script', + _id: 'script.js.test_setStateDelayed_nested', + type: 'script', common: { - name: 'test setStateDelayed nested', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_setStateDelayed_nested', { type: 'number', read: true, write: false }, () => {\n` + - ` setStateDelayed('test_setStateDelayed_nested', 6, 500);\n` + - ` setStateDelayed('test_setStateDelayed_nested', 7, 1500, false);` + - `});`, + name: 'test setStateDelayed nested', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_setStateDelayed_nested', { type: 'number', read: true, write: false }, () => {\n` + + ` setStateDelayed('test_setStateDelayed_nested', 6, 500);\n` + + ` setStateDelayed('test_setStateDelayed_nested', 7, 1500, false);` + + `});`, }, - native: {} + native: {}, }; let start = 0; @@ -903,40 +935,46 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_setStateDelayed_overwrite', - type: 'script', + _id: 'script.js.test_setStateDelayed_overwrite', + type: 'script', common: { - name: 'test setStateDelayed overwrite', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_setStateDelayed_overwrite', { type: 'number', read: true, write: false }, () => {\n` + - ` setStateDelayed('test_setStateDelayed_overwrite', 8, 500);\n` + - ` setStateDelayed('test_setStateDelayed_overwrite', 9, 1500);` + - `});`, + name: 'test setStateDelayed overwrite', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_setStateDelayed_overwrite', { type: 'number', read: true, write: false }, () => {\n` + + ` setStateDelayed('test_setStateDelayed_overwrite', 8, 500);\n` + + ` setStateDelayed('test_setStateDelayed_overwrite', 9, 1500);` + + `});`, }, - native: {} + native: {}, }; objects.setObject(script._id, script, err => { expect(err).to.be.null; - checkValueOfState('javascript.0.test_setStateDelayed_overwrite', 8, err => { - expect(err).to.be.ok; + checkValueOfState( + 'javascript.0.test_setStateDelayed_overwrite', + 8, + err => { + expect(err).to.be.ok; - states.getState('javascript.0.test_setStateDelayed_overwrite', (err, stateStart) => { - expect(err).to.be.null; - expect(stateStart.val).to.be.not.equal(8); - - checkValueOfState('javascript.0.test_setStateDelayed_overwrite', 9, err => { + states.getState('javascript.0.test_setStateDelayed_overwrite', (err, stateStart) => { expect(err).to.be.null; - states.getState('javascript.0.test_setStateDelayed_overwrite', err => { + expect(stateStart.val).to.be.not.equal(8); + + checkValueOfState('javascript.0.test_setStateDelayed_overwrite', 9, err => { expect(err).to.be.null; - done(); + states.getState('javascript.0.test_setStateDelayed_overwrite', err => { + expect(err).to.be.null; + done(); + }); }); }); - }); - }, 18); + }, + 18, + ); }); }); @@ -944,34 +982,40 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_clearStateDelayed', - type: 'script', + _id: 'script.js.test_clearStateDelayed', + type: 'script', common: { - name: 'test clearStateDelayed', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_clearStateDelayed', { type: 'number', read: true, write: false }, () => {\n` + - ` setStateDelayed('test_clearStateDelayed', 10, 500);\n` + - ` clearStateDelayed('test_clearStateDelayed');` + - `});`, + name: 'test clearStateDelayed', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_clearStateDelayed', { type: 'number', read: true, write: false }, () => {\n` + + ` setStateDelayed('test_clearStateDelayed', 10, 500);\n` + + ` clearStateDelayed('test_clearStateDelayed');` + + `});`, }, - native: {} + native: {}, }; objects.setObject(script._id, script, err => { expect(err).to.be.null; - checkValueOfState('javascript.0.test_clearStateDelayed', 10, err => { - expect(err).to.be.ok; + checkValueOfState( + 'javascript.0.test_clearStateDelayed', + 10, + err => { + expect(err).to.be.ok; - states.getState('javascript.0.test_clearStateDelayed', (err, stateStart) => { - expect(err).to.be.null; - expect(stateStart.val).to.be.not.equal(10); - done(); - }); - }, 18); + states.getState('javascript.0.test_clearStateDelayed', (err, stateStart) => { + expect(err).to.be.null; + expect(stateStart.val).to.be.not.equal(10); + done(); + }); + }, + 18, + ); }); }); @@ -979,22 +1023,23 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_getStateDelayed_single', - type: 'script', + _id: 'script.js.test_getStateDelayed_single', + type: 'script', common: { - name: 'test getStateDelayed single', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_getStateDelayed_single', { type: 'number', read: true, write: false }, () => {\n` + - ` createState('test_getStateDelayed_single_result', '', () => {\n` + - ` setStateDelayed('test_getStateDelayed_single', 10, 1500);\n` + - ` setState('test_getStateDelayed_single_result', JSON.stringify(getStateDelayed('test_getStateDelayed_single')));\n` + - ` });\n` + - `});`, + name: 'test getStateDelayed single', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_getStateDelayed_single', { type: 'number', read: true, write: false }, () => {\n` + + ` createState('test_getStateDelayed_single_result', '', () => {\n` + + ` setStateDelayed('test_getStateDelayed_single', 10, 1500);\n` + + ` setState('test_getStateDelayed_single_result', JSON.stringify(getStateDelayed('test_getStateDelayed_single')));\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; objects.setObject(script._id, script, err => { @@ -1019,22 +1064,23 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_getStateDelayed_all', - type: 'script', + _id: 'script.js.test_getStateDelayed_all', + type: 'script', common: { - name: 'test getStateDelayed all', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_getStateDelayed_all', { type: 'number', read: true, write: false }, () => {\n` + - ` createState('test_getStateDelayed_all_result', '{}', () => {\n` + - ` setStateDelayed('test_getStateDelayed_all', 11, 2500);\n` + - ` setState('test_getStateDelayed_all_result', JSON.stringify(getStateDelayed()));\n` + - ` });\n` + - `});`, + name: 'test getStateDelayed all', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_getStateDelayed_all', { type: 'number', read: true, write: false }, () => {\n` + + ` createState('test_getStateDelayed_all_result', '{}', () => {\n` + + ` setStateDelayed('test_getStateDelayed_all', 11, 2500);\n` + + ` setState('test_getStateDelayed_all_result', JSON.stringify(getStateDelayed()));\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; objects.setObject(script._id, script, err => { @@ -1059,48 +1105,56 @@ describe.only('Test JS', function () { this.timeout(5000); // add script const script = { - _id: 'script.js.test_setStateChanged', - type: 'script', + _id: 'script.js.test_setStateChanged', + type: 'script', common: { - name: 'test setStateChanged', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_setStateChanged', 4, () => {\n` + - ` setTimeout(() => { setStateChanged('test_setStateChanged', 4, true); }, 500);\n` + - ` setTimeout(() => { setStateChanged('test_setStateChanged', 5, true); }, 1000);\n` + - ` setTimeout(() => { setState('test_setStateChanged', 5, true); }, 1500);\n` + - `});`, + name: 'test setStateChanged', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_setStateChanged', 4, () => {\n` + + ` setTimeout(() => { setStateChanged('test_setStateChanged', 4, true); }, 500);\n` + + ` setTimeout(() => { setStateChanged('test_setStateChanged', 5, true); }, 1000);\n` + + ` setTimeout(() => { setState('test_setStateChanged', 5, true); }, 1500);\n` + + `});`, }, - native: {} + native: {}, }; let start = 0, count = 0; - const onStateChanged = function (id, state){ + const onStateChanged = function (id, state) { if (id !== 'javascript.0.test_setStateChanged') return; - if (state.val === 4) { // has to be called once - on state creation - if (start !== 0) {// on state change by setStateChanged('changed', 4, true) - should not be run, as it not change the state, including `state.ts` + if (state.val === 4) { + // has to be called once - on state creation + if (start !== 0) { + // on state change by setStateChanged('changed', 4, true) - should not be run, as it not change the state, including `state.ts` count++; } expect(start).to.be.equal(0); - if (start === 0) { // on state creation + if (start === 0) { + // on state creation start = state.ts; } - } else if (state.val === 5) { // has to be called twice - on state change by setStateChanged('changed', 5, true) and setState('changed', 5, true) - if (count === 0) { // on state change by setStateChanged('changed', 5, true) + } else if (state.val === 5) { + // has to be called twice - on state change by setStateChanged('changed', 5, true) and setState('changed', 5, true) + if (count === 0) { + // on state change by setStateChanged('changed', 5, true) count++; expect(state.ts - start).to.be.least(950); expect(state.ts - start).to.be.below(1450); - } else if (count === 1) { // on state change by setState('changed', 5, true) + } else if (count === 1) { + // on state change by setState('changed', 5, true) count++; expect(state.ts - start).to.be.least(1450); removeStateChangedHandler(onStateChanged); setTimeout(done, 100); } - if (count === 2) { // exit in any case + if (count === 2) { + // exit in any case removeStateChangedHandler(onStateChanged); } } @@ -1113,32 +1167,33 @@ describe.only('Test JS', function () { this.timeout(1000); // add script const script = { - _id: 'script.js.test_selector_toArray', - type: 'script', + _id: 'script.js.test_selector_toArray', + type: 'script', common: { - name: 'test selector toArray', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('selector.test_1.state', true, () => {\n` + - ` createState('selector.test_2.state', false, () => {\n` + - ` createState('selector.test_3.state', true, () => {\n` + - ` createState('selector.test_4.id', true, () => {\n` + - ` const states = $('state[id=javascript.0.selector.test_*.state]')` + - ` .toArray().filter((id) => getState(id)?.val === true);\n` + - ` if (Array.isArray(states)) {\n` + - ` createState('test_selector_toArray', states.length, true);\n` + - ` }\n` + - ` });\n` + - ` });\n` + - ` });\n` + - `});`, + name: 'test selector toArray', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('selector.test_1.state', true, () => {\n` + + ` createState('selector.test_2.state', false, () => {\n` + + ` createState('selector.test_3.state', true, () => {\n` + + ` createState('selector.test_4.id', true, () => {\n` + + ` const states = $('state[id=javascript.0.selector.test_*.state]')` + + ` .toArray().filter((id) => getState(id)?.val === true);\n` + + ` if (Array.isArray(states)) {\n` + + ` createState('test_selector_toArray', states.length, true);\n` + + ` }\n` + + ` });\n` + + ` });\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; - const onStateChanged = function (id, state){ + const onStateChanged = function (id, state) { if (id !== 'javascript.0.test_selector_toArray') return; removeStateChangedHandler(onStateChanged); expect(state.val).to.be.equal(2); @@ -1148,22 +1203,21 @@ describe.only('Test JS', function () { objects.setObject(script._id, script); }); - it('Test JS: test stopScript', function (done) { this.timeout(5000); // add script const script = { - _id: 'script.js.stopScript', - type: 'script', + _id: 'script.js.stopScript', + type: 'script', common: { - name: 'stopScript', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `stopScript('stopScript');`, + name: 'stopScript', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `stopScript('stopScript');`, }, - native: {} + native: {}, }; objects.setObject(script._id, script, err => { @@ -1181,30 +1235,30 @@ describe.only('Test JS', function () { it('Test JS: test startScript', function (done) { // add script const script = { - _id: 'script.js.startScript', - type: 'script', + _id: 'script.js.startScript', + type: 'script', common: { - name: 'startScript', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `startScript('stopScript');`, + name: 'startScript', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `startScript('stopScript');`, }, - native: {} + native: {}, }; const stopScript = { - _id: 'script.js.stopScript', - type: 'script', + _id: 'script.js.stopScript', + type: 'script', common: { - name: 'stopScript', - enabled: false, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `console.log('started script');`, + name: 'stopScript', + enabled: false, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `console.log('started script');`, }, - native: {} + native: {}, }; objects.setObject(stopScript._id, stopScript, err => { @@ -1232,52 +1286,58 @@ describe.only('Test JS', function () { it('Test JS: test global function globalSetTestState', done => { // add script const script = { - _id: 'script.js.test_globalSetTestState', - type: 'script', + _id: 'script.js.test_globalSetTestState', + type: 'script', common: { - name: 'test global function globalSetTestState', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `globalSetTestState(16);`, + name: 'test global function globalSetTestState', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `globalSetTestState(16);`, }, - native: {} + native: {}, }; objects.setObject(script._id, script, err => { expect(err).to.be.null; - checkValueOfState('javascript.0.test_global_setTestState', 16, err => { - expect(err).to.be.null; - - states.getState('javascript.0.test_global_setTestState', (err, state) => { + checkValueOfState( + 'javascript.0.test_global_setTestState', + 16, + err => { expect(err).to.be.null; - expect(state).to.be.ok; - expect(state.val).to.be.equal(16); - done(); - }); - }, 18); + + states.getState('javascript.0.test_global_setTestState', (err, state) => { + expect(err).to.be.null; + expect(state).to.be.ok; + expect(state.val).to.be.equal(16); + done(); + }); + }, + 18, + ); }); }).timeout(5000); it('Test JS: test ON default', function (done) { // add script const script = { - _id: 'script.js.test_ON_default', - type: 'script', + _id: 'script.js.test_ON_default', + type: 'script', common: { - name: 'test ON default', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('testResponse', false);\n` + - `createState('testVar', 0, () => {\n` + - ` on('testVar', (obj) => {\n` + - ` setState('testResponse', obj.state.val, true);\n` + - ` });\n` + - `});`, + name: 'test ON default', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('testResponse', false);\n` + + `createState('testVar', 0, () => {\n` + + ` on('testVar', (obj) => {\n` + + ` setState('testResponse', obj.state.val, true);\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { @@ -1303,17 +1363,17 @@ describe.only('Test JS', function () { it('Test JS: test ON any', function (done) { // add script const script = { - _id: 'script.js.test_ON_any', - type: 'script', + _id: 'script.js.test_ON_any', + type: 'script', common: { - name: 'test ON any', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('testResponse1', false); createState('testVar1', 1, () => { on({ id:'testVar1', change:'any' }, (obj) => { setState('testResponse1', obj.state.val, true); }); });`, + name: 'test ON any', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `createState('testResponse1', false); createState('testVar1', 1, () => { on({ id:'testVar1', change:'any' }, (obj) => { setState('testResponse1', obj.state.val, true); }); });`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { @@ -1339,79 +1399,118 @@ describe.only('Test JS', function () { it('Test JS: test ON misc', function (done) { //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - function scriptFunction (param) { + function scriptFunction(param) { let results = ''; const TEST_VAR = 'javascript.0.device.channel.testVar'; const TEST_RESULTS = 'javascript.0.testResults'; const recs = [ // request Options // on options or iD // states to set - [ { no: 1, cnt: 2, val: true }, { id: /\.testVar$/, val: true }, [ true, false, { val: true, ack: true } ] ], - [ { no: 2, cnt: 2, val: true }, { id: 0, val: true }, [ true, false, { val: true, ack: true } ] ], - [ { no: 3, cnt: 2, val: false, tio: 2}, { id: 0, val: false }, [ true, false, { val: true, ack: true }, { val: false, ack: true } ] ], - [ { no: 4, cnt: 1, val: {val: true, ack: true }}, { id: 0, val: true, ack: true }, [ true, false, { val: true, ack: true }, { val: false, ack: true } ] ], - [ { no: 5, cnt: 1, val: {val:false, ack: true }}, { id: 0, val: false, ack: true }, [ true, false, { val: true, ack: true }, { val: false, ack: true } ] ], - [ { no: 6, cnt: 1, val: true }, { id: 0, change: 'ne' }, [ false, true, true ]], - [ { no: 7, cnt: 2, val: true }, { id: 0, change: 'any' }, [ true, true ]], - [ { no: 8, cnt: 1, val: true }, { id: 0, change: 'gt' }, [ false, true, true ]], - [ { no: 9, cnt: 2, val: true }, { id: 0, change: 'eq' }, [ true, true, true, false ]], - [ { no: 10, cnt: 1, val: 'World' }, { name: 'Hello', change: 'gt' }, ['Change', 'World', 'World'] ], - [ { no: 11, cnt: 0, val: 'World' }, { name: 'hello', change: 'gt' }, ['Change', 'World', 'World'] ], - [ { no: 12, cnt: 1, val: 'World' }, { name: /^[h|H]ello/, change: 'any' }, ['World'] ], - - [ { no: 13, cnt: 1, val: 'B' }, { id: 0, valGt: 'A' }, [ 'B', 'A'] ], - [ { no: 14, cnt: 2, val: 'B' }, { id: 0, valGe: 'A' }, [ 'B', 'B'] ], - [ { no: 15, cnt: 1, val: 'B' }, { id: 0, valGe: 'B' }, [ 'B', 'A'] ], - [ { no: 16, cnt: 1, val: 'A' }, { id: 0, valLt: 'B' }, [ 'A', 'C'] ], - [ { no: 17, cnt: 1, val: 'A' }, { id: 0, valLe: 'A' }, [ 'A', 'B'] ], - [ { no: 18, cnt: 1, val: 'B' }, { id: 0, valNe: 'A' }, [ 'B', 'A'] ], - [ { no: 19, cnt: 1, val: 'onChannel' }, { channelId: 'javascript.0.device.channel' }, [ 'onChannel'] ], - [ { no: 20, cnt: 1, val: 'onChannel'}, { channelId: 'javascript.0.device.channel', val: 'onChannel' }, [ 'onChannel', 'xyz'] ], - [ { no: 21, cnt: 1, val: 'onChannel'}, { channelName: 'Channel' }, [ 'onChannel'] ], - [ { no: 22, cnt: 1, val: 'onChannel'}, { channelName: 'Channel', val: 'onChannel' }, [ 'onChannel', 'xyz'] ], - [ { no: 23, cnt: 1, val: 'onDevice'}, { deviceId: 'javascript.0.device' }, [ 'onDevice'] ], - [ { no: 24, cnt: 1, val: 'onDevice'}, { deviceId: 'javascript.0.device', val: 'onDevice' }, [ 'onDevice', 'xyz'] ], - [ { no: 25, cnt: 1, val: 'onDevice'}, { deviceName: 'Device' }, [ 'onDevice'] ], - [ { no: 26, cnt: 1, val: 'onDevice'}, { deviceName: 'Device', val: 'onDevice' }, [ 'onDevice', 'xyz'] ], - - [ { no: 27, cnt: 1, val: 1, before: false }, { id:0, oldVal: false }, [ 1, 1 ] ], - [ { no: 28, cnt: 1, val: 1, before: 2 }, { id:0, oldValGt: 1 }, [ 1, 1 ] ], - [ { no: 29, cnt: 2, val: 1, before: 2, tio: 2 }, { id:0, oldValGe: 1 }, [ 1, 1 ] ], - [ { no: 30, cnt: 1, before: 2 }, { id:0, oldValNe: 1 }, [ 1, 0 ] ], - [ { no: 31, cnt: 1, before: 0 }, { id:0, oldValLt: 1 }, [ 1, 0 ] ], - [ { no: 32, cnt: 2, before: 0 }, { id:0, oldValLe: 1 }, [ 1, 2, 0] ], - - [ { no: 33, cnt: 1, val: 1 }, { id:0, tsGt: 1 }, [ 1 ] ], - [ { no: 34, cnt: 0 }, { id:0, tsGt: 0xfffffffffff }, [ 1 ] ], - [ { no: 35, cnt: 1, val: 1 }, { id:0, tsLt: 0xfffffffffff }, [ 1 ] ], - [ { no: 36, cnt: 0 }, { id:0, tsLt: 1 }, [ 1 ] ], - [ { no: 37, cnt: 1, val: 1 }, { id:0, oldTsGt: 1 }, [ 1 ] ], - [ { no: 38, cnt: 0 }, { id:0, oldTsGt: 0xfffffffffff }, [ 1 ] ], - [ { no: 39, cnt: 1, val: 1 }, { id:0, oldTsLt: 0xfffffffffff }, [ 1 ] ], - [ { no: 40, cnt: 0 }, { id:0, oldTsLt: 1 }, [ 1 ] ], - [ { no: 41, cnt: 1, val: 1 }, { id:0, lcGt: 1 }, [ 1 ] ], - [ { no: 42, cnt: 1, val: 1 }, { id:0, lcLt: 0xfffffffffff }, [ 1 ] ], - [ { no: 43, cnt: 0 }, { id:0, lcLt: 1 }, [ 1 ] ], - [ { no: 44, cnt: 1, val: 1 }, { id:0, oldLcGt: 1 }, [ 1 ] ], - [ { no: 45, cnt: 0 }, { id:0, oldLcGt: 0xfffffffffff }, [ 1 ] ], - [ { no: 46, cnt: 1, val: 1 }, { id:0, oldLcLt: 0xfffffffffff }, [ 1 ] ], - [ { no: 47, cnt: 0 }, { id:0, oldLcLt: 1 }, [ 1 ] ], - - [ { no: 48, cnt: 1, val: 1 }, { id:0, from: 'system.adapter.javascript.0' }, [ 1 ] ], - [ { no: 49, cnt: 0 }, { id:0, from: 'system.adapter.javascript.1' }, [ 1 ] ], - [ { no: 50, cnt: 1, val: 1 }, { id:0, oldFrom: 'system.adapter.javascript.0' }, [ 1 ] ], - [ { no: 51, cnt: 0 }, { id:0, oldFrom: 'system.adapter.javascript.1' }, [ 1 ] ], + [ + { no: 1, cnt: 2, val: true }, + { id: /\.testVar$/, val: true }, + [true, false, { val: true, ack: true }], + ], + [{ no: 2, cnt: 2, val: true }, { id: 0, val: true }, [true, false, { val: true, ack: true }]], + [ + { no: 3, cnt: 2, val: false, tio: 2 }, + { id: 0, val: false }, + [true, false, { val: true, ack: true }, { val: false, ack: true }], + ], + [ + { no: 4, cnt: 1, val: { val: true, ack: true } }, + { id: 0, val: true, ack: true }, + [true, false, { val: true, ack: true }, { val: false, ack: true }], + ], + [ + { no: 5, cnt: 1, val: { val: false, ack: true } }, + { id: 0, val: false, ack: true }, + [true, false, { val: true, ack: true }, { val: false, ack: true }], + ], + [{ no: 6, cnt: 1, val: true }, { id: 0, change: 'ne' }, [false, true, true]], + [{ no: 7, cnt: 2, val: true }, { id: 0, change: 'any' }, [true, true]], + [{ no: 8, cnt: 1, val: true }, { id: 0, change: 'gt' }, [false, true, true]], + [{ no: 9, cnt: 2, val: true }, { id: 0, change: 'eq' }, [true, true, true, false]], + [{ no: 10, cnt: 1, val: 'World' }, { name: 'Hello', change: 'gt' }, ['Change', 'World', 'World']], + [{ no: 11, cnt: 0, val: 'World' }, { name: 'hello', change: 'gt' }, ['Change', 'World', 'World']], + [{ no: 12, cnt: 1, val: 'World' }, { name: /^[h|H]ello/, change: 'any' }, ['World']], + + [{ no: 13, cnt: 1, val: 'B' }, { id: 0, valGt: 'A' }, ['B', 'A']], + [{ no: 14, cnt: 2, val: 'B' }, { id: 0, valGe: 'A' }, ['B', 'B']], + [{ no: 15, cnt: 1, val: 'B' }, { id: 0, valGe: 'B' }, ['B', 'A']], + [{ no: 16, cnt: 1, val: 'A' }, { id: 0, valLt: 'B' }, ['A', 'C']], + [{ no: 17, cnt: 1, val: 'A' }, { id: 0, valLe: 'A' }, ['A', 'B']], + [{ no: 18, cnt: 1, val: 'B' }, { id: 0, valNe: 'A' }, ['B', 'A']], + [{ no: 19, cnt: 1, val: 'onChannel' }, { channelId: 'javascript.0.device.channel' }, ['onChannel']], + [ + { no: 20, cnt: 1, val: 'onChannel' }, + { channelId: 'javascript.0.device.channel', val: 'onChannel' }, + ['onChannel', 'xyz'], + ], + [{ no: 21, cnt: 1, val: 'onChannel' }, { channelName: 'Channel' }, ['onChannel']], + [ + { no: 22, cnt: 1, val: 'onChannel' }, + { channelName: 'Channel', val: 'onChannel' }, + ['onChannel', 'xyz'], + ], + [{ no: 23, cnt: 1, val: 'onDevice' }, { deviceId: 'javascript.0.device' }, ['onDevice']], + [ + { no: 24, cnt: 1, val: 'onDevice' }, + { deviceId: 'javascript.0.device', val: 'onDevice' }, + ['onDevice', 'xyz'], + ], + [{ no: 25, cnt: 1, val: 'onDevice' }, { deviceName: 'Device' }, ['onDevice']], + [{ no: 26, cnt: 1, val: 'onDevice' }, { deviceName: 'Device', val: 'onDevice' }, ['onDevice', 'xyz']], + + [{ no: 27, cnt: 1, val: 1, before: false }, { id: 0, oldVal: false }, [1, 1]], + [{ no: 28, cnt: 1, val: 1, before: 2 }, { id: 0, oldValGt: 1 }, [1, 1]], + [{ no: 29, cnt: 2, val: 1, before: 2, tio: 2 }, { id: 0, oldValGe: 1 }, [1, 1]], + [{ no: 30, cnt: 1, before: 2 }, { id: 0, oldValNe: 1 }, [1, 0]], + [{ no: 31, cnt: 1, before: 0 }, { id: 0, oldValLt: 1 }, [1, 0]], + [{ no: 32, cnt: 2, before: 0 }, { id: 0, oldValLe: 1 }, [1, 2, 0]], + + [{ no: 33, cnt: 1, val: 1 }, { id: 0, tsGt: 1 }, [1]], + [{ no: 34, cnt: 0 }, { id: 0, tsGt: 0xfffffffffff }, [1]], + [{ no: 35, cnt: 1, val: 1 }, { id: 0, tsLt: 0xfffffffffff }, [1]], + [{ no: 36, cnt: 0 }, { id: 0, tsLt: 1 }, [1]], + [{ no: 37, cnt: 1, val: 1 }, { id: 0, oldTsGt: 1 }, [1]], + [{ no: 38, cnt: 0 }, { id: 0, oldTsGt: 0xfffffffffff }, [1]], + [{ no: 39, cnt: 1, val: 1 }, { id: 0, oldTsLt: 0xfffffffffff }, [1]], + [{ no: 40, cnt: 0 }, { id: 0, oldTsLt: 1 }, [1]], + [{ no: 41, cnt: 1, val: 1 }, { id: 0, lcGt: 1 }, [1]], + [{ no: 42, cnt: 1, val: 1 }, { id: 0, lcLt: 0xfffffffffff }, [1]], + [{ no: 43, cnt: 0 }, { id: 0, lcLt: 1 }, [1]], + [{ no: 44, cnt: 1, val: 1 }, { id: 0, oldLcGt: 1 }, [1]], + [{ no: 45, cnt: 0 }, { id: 0, oldLcGt: 0xfffffffffff }, [1]], + [{ no: 46, cnt: 1, val: 1 }, { id: 0, oldLcLt: 0xfffffffffff }, [1]], + [{ no: 47, cnt: 0 }, { id: 0, oldLcLt: 1 }, [1]], + + [{ no: 48, cnt: 1, val: 1 }, { id: 0, from: 'system.adapter.javascript.0' }, [1]], + [{ no: 49, cnt: 0 }, { id: 0, from: 'system.adapter.javascript.1' }, [1]], + [{ no: 50, cnt: 1, val: 1 }, { id: 0, oldFrom: 'system.adapter.javascript.0' }, [1]], + [{ no: 51, cnt: 0 }, { id: 0, oldFrom: 'system.adapter.javascript.1' }, [1]], // not ok with the old patternMatching function - [ { no: 52, cnt: 1, val: 'onChannel'}, { channelId: /^javascript.0.device.channel$/ }, [ 'onChannel'] ], - [ { no: 53, cnt: 1, val: 'onChannel'}, { channelId: /^javascript.0.device.channel$/, val: 'onChannel' }, [ 'onChannel', 'xyz'] ], - [ { no: 54, cnt: 1, val: 'onChannel'}, { channelName: /^Channel$/ }, [ 'onChannel'] ], - [ { no: 55, cnt: 1, val: 'onChannel'}, { channelName: /^Channel$/, val: 'onChannel' }, [ 'onChannel', 'xyz'] ], - [ { no: 56, cnt: 1, val: 'onDevice'}, { deviceId: /^javascript.0.device$/ }, [ 'onDevice'] ], - [ { no: 57, cnt: 1, val: 'onDevice'}, { deviceId: /^javascript.0.device$/, val: 'onDevice' }, [ 'onDevice', 'xyz'] ], - [ { no: 58, cnt: 1, val: 'onDevice'}, { deviceName: /^Device$/ }, [ 'onDevice'] ], - [ { no: 59, cnt: 1, val: 'onDevice'}, { deviceName: /^Device$/, val: 'onDevice' }, [ 'onDevice', 'xyz'] ] - + [{ no: 52, cnt: 1, val: 'onChannel' }, { channelId: /^javascript.0.device.channel$/ }, ['onChannel']], + [ + { no: 53, cnt: 1, val: 'onChannel' }, + { channelId: /^javascript.0.device.channel$/, val: 'onChannel' }, + ['onChannel', 'xyz'], + ], + [{ no: 54, cnt: 1, val: 'onChannel' }, { channelName: /^Channel$/ }, ['onChannel']], + [ + { no: 55, cnt: 1, val: 'onChannel' }, + { channelName: /^Channel$/, val: 'onChannel' }, + ['onChannel', 'xyz'], + ], + [{ no: 56, cnt: 1, val: 'onDevice' }, { deviceId: /^javascript.0.device$/ }, ['onDevice']], + [ + { no: 57, cnt: 1, val: 'onDevice' }, + { deviceId: /^javascript.0.device$/, val: 'onDevice' }, + ['onDevice', 'xyz'], + ], + [{ no: 58, cnt: 1, val: 'onDevice' }, { deviceName: /^Device$/ }, ['onDevice']], + [{ no: 59, cnt: 1, val: 'onDevice' }, { deviceName: /^Device$/, val: 'onDevice' }, ['onDevice', 'xyz']], ]; switch (param) { @@ -1432,13 +1531,19 @@ describe.only('Test JS', function () { } function handler(result, req, obj) { - log ('handler: result=' + JSON.stringify(result) + ' / req=' + JSON.stringify(req) + ' / obj=' + JSON.stringify(obj)); - if (obj.state.ts <= result.initTs && - ( - (obj.state.val === result.before && obj.state.ack === result.ack) || - (obj.state.val === '___' && obj.state.ack === true) // createState event - ) - ){ + log( + 'handler: result=' + + JSON.stringify(result) + + ' / req=' + + JSON.stringify(req) + + ' / obj=' + + JSON.stringify(obj), + ); + if ( + obj.state.ts <= result.initTs && + ((obj.state.val === result.before && obj.state.ack === result.ack) || + (obj.state.val === '___' && obj.state.ack === true)) // createState event + ) { // we got the value subscribe for the "start" or "createState" value too, ignore it log('IGNORED'); return; @@ -1446,12 +1551,11 @@ describe.only('Test JS', function () { if (typeof result.val === 'object') { Object.keys(result.val).forEach(n => { addResult('obj.state.' + n + '=' + obj.state[n] + ' val.' + n + '=' + result.val[n]); - result.nok = result.nok || (result.val[n] !== obj.state[n]); + result.nok = result.nok || result.val[n] !== obj.state[n]; }); - } else if (result.val !== undefined) { addResult('obj.state.val=' + obj.state.val + ' val=' + result.val); - result.nok = result.nok || (result.val !== obj.state.val); + result.nok = result.nok || result.val !== obj.state.val; } result.callCount += 1; } @@ -1492,7 +1596,16 @@ describe.only('Test JS', function () { if (no >= ar.length) { setTimeout(() => { unsubscribe(sub); - results = (req.callCount === req.cnt && req.nok === false ? 'OK;' : 'NOK;') + 'no=' + req.no + ';' + results + 'callCount=' + req.callCount + ';cnt=' + req.cnt; + results = + (req.callCount === req.cnt && req.nok === false ? 'OK;' : 'NOK;') + + 'no=' + + req.no + + ';' + + results + + 'callCount=' + + req.callCount + + ';cnt=' + + req.cnt; setState(TEST_RESULTS, results, true, callback); }, req.tio); @@ -1520,8 +1633,7 @@ describe.only('Test JS', function () { const rec = recs[cnt++]; // sometimes the state init event from createState will be received too, so wait a little - setTimeout(() => - createTest(rec[0], rec[1], rec[2], () => setTimeout(doIt, 1000)), 200); + setTimeout(() => createTest(rec[0], rec[1], rec[2], () => setTimeout(doIt, 1000)), 200); })(); }); } @@ -1531,17 +1643,17 @@ describe.only('Test JS', function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////// const script = { - _id: 'script.js.test_ON', - type: 'script', + _id: 'script.js.test_ON', + type: 'script', common: { - name: 'test ON any', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `${scriptFunction.toString()}\nscriptFunction();\n`, + name: 'test ON any', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: `${scriptFunction.toString()}\nscriptFunction();\n`, }, - native: {} + native: {}, }; const recs = scriptFunction('recs'); @@ -1577,7 +1689,7 @@ describe.only('Test JS', function () { if (cnt >= recs.length) { removeStateChangedHandler(onStateChanged); - done (); + done(); } } }; @@ -1595,20 +1707,21 @@ describe.only('Test JS', function () { console.log('Must wait 2 seconds[' + ((d.getSeconds() + 2) % 60) + ' * * * * *]' + d.toISOString()); // add script const script = { - _id: 'script.js.test_schedule_seconds', - type: 'script', + _id: 'script.js.test_schedule_seconds', + type: 'script', common: { - name: 'test schedule seconds', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_schedule_seconds', false);\n` + - `schedule('${(d.getSeconds() + 2) % 60} * * * * *', () => {\n` + - ` setState('test_schedule_seconds', true, true);\n` + - `});`, + name: 'test schedule seconds', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_schedule_seconds', false);\n` + + `schedule('${(d.getSeconds() + 2) % 60} * * * * *', () => {\n` + + ` setState('test_schedule_seconds', true, true);\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { @@ -1626,25 +1739,33 @@ describe.only('Test JS', function () { it('Test JS: test schedule minutes', function (done) { const d = new Date(); - console.log('Must wait ' + (60 - d.getSeconds()) + ' seconds[' + ((d.getMinutes() + 1) % 60) + ' * * * *] ' + d.toISOString()); + console.log( + 'Must wait ' + + (60 - d.getSeconds()) + + ' seconds[' + + ((d.getMinutes() + 1) % 60) + + ' * * * *] ' + + d.toISOString(), + ); this.timeout((64 - d.getSeconds()) * 1000); // add script const script = { - _id: 'script.js.test_schedule_minutes', - type: 'script', + _id: 'script.js.test_schedule_minutes', + type: 'script', common: { - name: 'test schedule minutes', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_schedule_minutes', false);\n` + - `schedule('${(d.getMinutes() + 1) % 60} * * * *', () => {\n` + - ` setState('test_schedule_minutes', true, true);\n` + - `});`, + name: 'test schedule minutes', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_schedule_minutes', false);\n` + + `schedule('${(d.getMinutes() + 1) % 60} * * * *', () => {\n` + + ` setState('test_schedule_minutes', true, true);\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { @@ -1667,26 +1788,27 @@ describe.only('Test JS', function () { // add script const script = { - _id: 'script.js.test_scheduleById', - type: 'script', + _id: 'script.js.test_scheduleById', + type: 'script', common: { - name: 'test scheduleById', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_scheduleById', '00:00', { type: 'string', role: 'value.time', read: true, write: true }, () => {\n` + - ` createState('test_scheduleById_result', false, () => {\n` + - ` scheduleById('test_scheduleById', () => {\n` + - ` setState('test_scheduleById_result', { val: true, ack: true });\n` + - ` });\n` + - ` setTimeout(() => {\n` + - ` setState('test_scheduleById', '${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}', true);\n` + - ` }, 500);\n` + - ` });\n` + - `});`, + name: 'test scheduleById', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_scheduleById', '00:00', { type: 'string', role: 'value.time', read: true, write: true }, () => {\n` + + ` createState('test_scheduleById_result', false, () => {\n` + + ` scheduleById('test_scheduleById', () => {\n` + + ` setState('test_scheduleById_result', { val: true, ack: true });\n` + + ` });\n` + + ` setTimeout(() => {\n` + + ` setState('test_scheduleById', '${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}', true);\n` + + ` }, 500);\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { @@ -1705,23 +1827,24 @@ describe.only('Test JS', function () { it('Test JS: test writeFile to "0_userdata.0"', function (done) { // add script const script = { - _id: 'script.js.test_write_userdata', - type: 'script', + _id: 'script.js.test_write_userdata', + type: 'script', common: { - name: 'test file write', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_write_userdata', false, () => {\n` + - ` writeFile('0_userdata.0', 'test.txt', 'it works', (err) => {\n` + - ` if (!err) {\n` + - ` setState('test_write_userdata', true, true);\n` + - ` }\n` + - ` });\n` + - `});`, + name: 'test file write', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_write_userdata', false, () => {\n` + + ` writeFile('0_userdata.0', 'test.txt', 'it works', (err) => {\n` + + ` if (!err) {\n` + + ` setState('test_write_userdata', true, true);\n` + + ` }\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { @@ -1740,23 +1863,24 @@ describe.only('Test JS', function () { it('Test JS: test readFile from "0_userdata.0"', function (done) { // add script const script = { - _id: 'script.js.test_read_userdata', - type: 'script', + _id: 'script.js.test_read_userdata', + type: 'script', common: { - name: 'test file read', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_read_userdata', '', () => {\n` + - ` readFile('0_userdata.0', 'test.txt', (err, data) => {\n` + - ` if (!err) {\n` + - ` setState('test_read_userdata', { val: data, ack: true });\n` + - ` }\n` + - ` });\n` + - `});`, + name: 'test file read', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_read_userdata', '', () => {\n` + + ` readFile('0_userdata.0', 'test.txt', (err, data) => {\n` + + ` if (!err) {\n` + + ` setState('test_read_userdata', { val: data, ack: true });\n` + + ` }\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; const onStateChanged = function (id, state) { @@ -1832,39 +1956,40 @@ describe.only('Test JS', function () { it('Test JS: messaging between scripts', done => { // add script const script = { - _id: 'script.js.test_messaging', - type: 'script', + _id: 'script.js.test_messaging', + type: 'script', common: { - name: 'test messaging', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('onMessage', false, () => {\n` + - ` createState('messageTo', false, () => {\n` + - ` createState('messageDeleted', false, () => {\n` + - ` let id = onMessage('messageName', (data, callback) => {\n` + - ` setState('javascript.0.onMessage', data, true);\n` + - ` callback(5);\n` + - ` });\n` + - ` messageTo('messageName', 6, result => {\n` + - ` setState('javascript.0.messageTo', result, true);\n` + - ` setState('javascript.0.messageDeleted', onMessageUnregister(id), true);\n` + - ` });\n` + - ` });\n` + - ` });\n` + - `});`, + name: 'test messaging', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('onMessage', false, () => {\n` + + ` createState('messageTo', false, () => {\n` + + ` createState('messageDeleted', false, () => {\n` + + ` let id = onMessage('messageName', (data, callback) => {\n` + + ` setState('javascript.0.onMessage', data, true);\n` + + ` callback(5);\n` + + ` });\n` + + ` messageTo('messageName', 6, result => {\n` + + ` setState('javascript.0.messageTo', result, true);\n` + + ` setState('javascript.0.messageDeleted', onMessageUnregister(id), true);\n` + + ` });\n` + + ` });\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; let count = 3; const onStateChanged = function (id, state) { console.log(`ON CHANGE. ${id} ${JSON.stringify(state)}`); if ( - (id === 'javascript.0.messageTo' && state.val === 5 && state.ack === true) || + (id === 'javascript.0.messageTo' && state.val === 5 && state.ack === true) || (id === 'javascript.0.messageDeleted' && state.val === true && state.ack === true) || - (id === 'javascript.0.onMessage' && state.val === 6 && state.ack === true) + (id === 'javascript.0.onMessage' && state.val === 6 && state.ack === true) ) { if (!--count) { removeStateChangedHandler(onStateChanged); @@ -1880,22 +2005,23 @@ describe.only('Test JS', function () { it('Test JS: subscribe on file', done => { // add script const script = { - _id: 'script.js.test_read1', - type: 'script', + _id: 'script.js.test_read1', + type: 'script', common: { - name: 'test onFile', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('file', false, () => {\n` + - ` onFile('vis.0', 'main/*', true, (id, fileName, size, fileData, mimeType) => {\n` + - ` setState('javascript.0.file', fileData.toString(), true);\n` + - ` offFile('vis.0', 'main/*');\n` + - ` });\n` + - `});`, + name: 'test onFile', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('file', false, () => {\n` + + ` onFile('vis.0', 'main/*', true, (id, fileName, size, fileData, mimeType) => {\n` + + ` setState('javascript.0.file', fileData.toString(), true);\n` + + ` offFile('vis.0', 'main/*');\n` + + ` });\n` + + `});`, }, - native: {} + native: {}, }; let fileReceived = false; @@ -1935,19 +2061,20 @@ describe.only('Test JS', function () { this.timeout(10000); // add script const script = { - _id: 'script.js.test_formatTimeDiff', - type: 'script', + _id: 'script.js.test_formatTimeDiff', + type: 'script', common: { - name: 'test formatTimeDiff', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_formatTimeDiff', { type: 'string', role: 'json', read: true, write: false }, () => {\n` + - ` const diff1 = formatTimeDiff(172800000 + 10800000 + 540000 + 15000, 'hh:mm:ss');\n` + - ` const diff2 = formatTimeDiff((172800000 + 10800000 + 540000 + 15000) * -1, 'mm:ss');\n` + - ` setState('test_formatTimeDiff', { val: JSON.stringify({ diff1, diff2 }), ack: true });\n` + - `});`, + name: 'test formatTimeDiff', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_formatTimeDiff', { type: 'string', role: 'json', read: true, write: false }, () => {\n` + + ` const diff1 = formatTimeDiff(172800000 + 10800000 + 540000 + 15000, 'hh:mm:ss');\n` + + ` const diff2 = formatTimeDiff((172800000 + 10800000 + 540000 + 15000) * -1, 'mm:ss');\n` + + ` setState('test_formatTimeDiff', { val: JSON.stringify({ diff1, diff2 }), ack: true });\n` + + `});`, }, native: {}, }; @@ -1974,21 +2101,22 @@ describe.only('Test JS', function () { this.timeout(10000); // add script const script = { - _id: 'script.js.test_getDateObject', - type: 'script', + _id: 'script.js.test_getDateObject', + type: 'script', common: { - name: 'test getDateObject', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_getDateObject', { type: 'string', role: 'json', read: true, write: false }, () => {\n` + - ` const now = Date.now();\n` + - ` const justHour = getDateObject('14').toISOString();\n` + - ` const timeToday = getDateObject('20:15').toISOString();\n` + - ` const byTimestamp = getDateObject(1716056595000).toISOString();\n` + // 2024-05-18T18:23:15.000Z - ` setState('test_getDateObject', { val: JSON.stringify({ now, justHour, timeToday, byTimestamp }), ack: true });\n` + - `});`, + name: 'test getDateObject', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_getDateObject', { type: 'string', role: 'json', read: true, write: false }, () => {\n` + + ` const now = Date.now();\n` + + ` const justHour = getDateObject('14').toISOString();\n` + + ` const timeToday = getDateObject('20:15').toISOString();\n` + + ` const byTimestamp = getDateObject(1716056595000).toISOString();\n` + // 2024-05-18T18:23:15.000Z + ` setState('test_getDateObject', { val: JSON.stringify({ now, justHour, timeToday, byTimestamp }), ack: true });\n` + + `});`, }, native: {}, }; @@ -2033,23 +2161,24 @@ describe.only('Test JS', function () { this.timeout(10000); // add script const script = { - _id: 'script.js.test_getAttr', - type: 'script', + _id: 'script.js.test_getAttr', + type: 'script', common: { - name: 'test getAttr', - enabled: true, - verbose: true, - engine: 'system.adapter.javascript.0', - engineType: 'Javascript/js', - source: `createState('test_getAttr', { type: 'string', role: 'json', read: true, write: false }, () => {\n` + - ` const attr1 = getAttr('{"level1":{"level2":"myVal"}}', 'level1.level2');\n` + - ` const attr2 = getAttr({ level1: { level2: { level3: 15 } } }, 'level1.level2.level3');\n` + - ` const attr3 = getAttr({ obj: { 'with-hyphen': { val: true } } }, 'obj.with-hyphen.val');\n` + - ` const attr4 = getAttr({ obj: { 'colon:0': { val: 'yes' } } }, 'obj.colon:0.val');\n` + - ` const attr5 = getAttr({ obj: { arr: ['one', 'two', 'three', 'four'] } }, 'obj.arr.2');\n` + - ` const attr6 = getAttr({ obj: { arr: [{ val: 1 }, { val: 2 }, { val: 3 }, { val: 4 }] } }, 'obj.arr.1.val');\n` + - ` setState('test_getAttr', { val: JSON.stringify({ attr1, attr2, attr3, attr4, attr5, attr6 }), ack: true });\n` + - `});`, + name: 'test getAttr', + enabled: true, + verbose: true, + engine: 'system.adapter.javascript.0', + engineType: 'Javascript/js', + source: + `createState('test_getAttr', { type: 'string', role: 'json', read: true, write: false }, () => {\n` + + ` const attr1 = getAttr('{"level1":{"level2":"myVal"}}', 'level1.level2');\n` + + ` const attr2 = getAttr({ level1: { level2: { level3: 15 } } }, 'level1.level2.level3');\n` + + ` const attr3 = getAttr({ obj: { 'with-hyphen': { val: true } } }, 'obj.with-hyphen.val');\n` + + ` const attr4 = getAttr({ obj: { 'colon:0': { val: 'yes' } } }, 'obj.colon:0.val');\n` + + ` const attr5 = getAttr({ obj: { arr: ['one', 'two', 'three', 'four'] } }, 'obj.arr.2');\n` + + ` const attr6 = getAttr({ obj: { arr: [{ val: 1 }, { val: 2 }, { val: 3 }, { val: 4 }] } }, 'obj.arr.1.val');\n` + + ` setState('test_getAttr', { val: JSON.stringify({ attr1, attr2, attr3, attr4, attr5, attr6 }), ack: true });\n` + + `});`, }, native: {}, }; diff --git a/test/testMirror.js b/test/testMirror.js index 8b43495ba..d9caa2db9 100644 --- a/test/testMirror.js +++ b/test/testMirror.js @@ -1,7 +1,7 @@ +const os = require('node:os'); +const path = require('node:path'); +const fs = require('node:fs'); const expect = require('chai').expect; -const os = require('os'); -const path = require('path'); -const fs = require('fs'); const Mirror = require('../lib/mirror'); describe('Mirror', () => { @@ -24,7 +24,7 @@ describe('Mirror', () => { }); describe('watchFolders', () => { - it('notifies about changes to normal files', (done) => { + it('notifies about changes to normal files', done => { const script = path.join(watched, 'script.js'); fs.closeSync(fs.openSync(script, 'w')); @@ -39,7 +39,7 @@ describe('Mirror', () => { fs.appendFileSync(script, 'some code'); }); - it('notifies about changes to symlinked files', (done) => { + it('notifies about changes to symlinked files', done => { // Script is located in an unwatched directory... const unwatched = fs.mkdtempSync(path.join(os.tmpdir(), 'mirror-test-unwatched-')); @@ -61,7 +61,7 @@ describe('Mirror', () => { fs.appendFileSync(script, 'some code'); }); - it('notifies about changes to symlinked directories', (done) => { + it('notifies about changes to symlinked directories', done => { // Script is located in an unwatched directory... const unwatched = fs.mkdtempSync(path.join(os.tmpdir(), 'mirror-test-unwatched-')); @@ -91,26 +91,18 @@ describe('Mirror', () => { fs.appendFileSync(script, 'some code'); }); - it('notifies about changes to relatively symlinked files', (done) => { + it('notifies about changes to relatively symlinked files', done => { // Script is located in an unwatched directory... - const unwatched = fs.mkdtempSync( - path.join(os.tmpdir(), 'mirror-test-unwatched-') - ); + const unwatched = fs.mkdtempSync(path.join(os.tmpdir(), 'mirror-test-unwatched-')); const script = path.join(unwatched, 'script.js'); fs.closeSync(fs.openSync(script, 'w')); // ...but symlinked as a file from a watched directory. const symlink = path.join(watched, 'symlinked-script.js'); - const relativeDirectory = path.relative( - path.dirname(symlink), - path.dirname(script) - ); + const relativeDirectory = path.relative(path.dirname(symlink), path.dirname(script)); - fs.symlinkSync( - path.join(relativeDirectory, path.basename(script)), - symlink - ); + fs.symlinkSync(path.join(relativeDirectory, path.basename(script)), symlink); mirror.onFileChange = (_event, file) => { expect(path.normalize(file)).to.equal(symlink); @@ -123,7 +115,7 @@ describe('Mirror', () => { fs.appendFileSync(script, 'some code'); }); - it('notifies about changes to relatively symlinked directories', (done) => { + it('notifies about changes to relatively symlinked directories', done => { // Script is located in an unwatched directory... const unwatched = fs.mkdtempSync(path.join(os.tmpdir(), 'mirror-test-unwatched-')); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js index c4b5af845..9d30d6371 100644 --- a/test/testPackageFiles.js +++ b/test/testPackageFiles.js @@ -1,7 +1,5 @@ -'use strict'; - +const fs = require('node:fs'); const expect = require('chai').expect; -const fs = require('node:fs'); describe('Test package.json and io-package.json', () => { it('Test package files', done => { @@ -19,10 +17,15 @@ describe('Test package.json and io-package.json', () => { expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; - expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + expect( + ioPackage.common.version, + 'ERROR: Version numbers in package.json and io-package.json needs to match', + ).to.be.equal(npmPackage.version); if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { - console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + console.log( + 'WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!', + ); console.log(); } @@ -33,20 +36,30 @@ describe('Test package.json and io-package.json', () => { if (ioPackage.common.name.indexOf('template') !== 0) { if (Array.isArray(ioPackage.common.authors)) { - expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + expect( + ioPackage.common.authors.length, + 'ERROR: Author in io-package.json needs to be set', + ).to.not.be.equal(0); if (ioPackage.common.authors.length === 1) { - expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + expect( + ioPackage.common.authors[0], + 'ERROR: Author in io-package.json needs to be a real name', + ).to.not.be.equal('my Name '); } + } else { + expect( + ioPackage.common.authors, + 'ERROR: Author in io-package.json needs to be a real name', + ).to.not.be.equal('my Name '); } - else { - expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); - } - } - else { + } else { console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); console.log(); } - expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + expect( + fs.existsSync(__dirname + '/../README.md'), + 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.', + ).to.be.true; if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { console.log('WARNING: titleLang is not existing in io-package.json. Please add'); console.log(); @@ -57,17 +70,26 @@ describe('Test package.json and io-package.json', () => { ioPackage.common.title.indexOf('adapter') !== -1 || ioPackage.common.title.indexOf('Adapter') !== -1 ) { - console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.'); + console.log( + 'WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.', + ); console.log(); } if (!ioPackage.common.controller && !ioPackage.common.onlyWWW && !ioPackage.common.noConfig) { - if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) { + if ( + !ioPackage.common.materialize || + !fs.existsSync(__dirname + '/../admin/index_m.html') || + !fs.existsSync(__dirname + '/../gulpfile.js') + ) { console.log('WARNING: Admin3 support is missing! Please add it'); console.log(); } if (ioPackage.common.materialize) { - expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true; + expect( + fs.existsSync(__dirname + '/../admin/index_m.html'), + 'Admin3 support is enabled in io-package.json, but index_m.html is missing!', + ).to.be.true; } } @@ -77,7 +99,10 @@ describe('Test package.json and io-package.json', () => { console.log('Warning: The README.md should have a section ## Changelog'); console.log(); } - expect((licenseFileExists || fileContentReadme.includes('## License')), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + expect( + licenseFileExists || fileContentReadme.includes('## License'), + 'A LICENSE must exist as LICENSE file or as part of the README.md', + ).to.be.true; if (!licenseFileExists) { console.log('Warning: The License should also exist as LICENSE file'); console.log(); diff --git a/test/testScheduler.js b/test/testScheduler.js index 0af2050b6..01ab87a97 100644 --- a/test/testScheduler.js +++ b/test/testScheduler.js @@ -1,10 +1,9 @@ const expect = require('chai').expect; -const Scheduler = require('../lib/scheduler'); const tk = require('timekeeper'); const suncalc = require('suncalc2'); +const Scheduler = require('../lib/scheduler'); describe('Test Scheduler', function () { - it('Test Scheduler: Should trigger on 23:59 every year', function (done) { const time = new Date(2030, 11, 31, 23, 58, 57); console.log('Wait ...'); @@ -12,10 +11,14 @@ describe('Test Scheduler', function () { console.log(new Date()); const s = new Scheduler(null, Date, suncalc); - s.add( '{"time":{"exactTime":true,"start":"23:59"},"period":{"years":1,"yearDate":31,"yearMonth":12}}', 'someName', () => { - console.log(new Date()); - done(); - }); + s.add( + '{"time":{"exactTime":true,"start":"23:59"},"period":{"years":1,"yearDate":31,"yearMonth":12}}', + 'someName', + () => { + console.log(new Date()); + done(); + }, + ); }).timeout(65000); it('Test Scheduler: Should not trigger on 23:59 every year day before', function (done) { @@ -25,9 +28,13 @@ describe('Test Scheduler', function () { console.log(new Date()); const s = new Scheduler(null, Date, suncalc); - s.add( '{"time":{"exactTime":true,"start":"23:59"},"period":{"years":1,"yearDate":31,"yearMonth":12}}', 'someName1', () => { - expect(false).to.be.true; - }); + s.add( + '{"time":{"exactTime":true,"start":"23:59"},"period":{"years":1,"yearDate":31,"yearMonth":12}}', + 'someName1', + () => { + expect(false).to.be.true; + }, + ); setTimeout(done, 5000); }).timeout(65000); @@ -35,7 +42,7 @@ describe('Test Scheduler', function () { const kcLat = 49.0068705; const kcLon = 8.4034195; const dat = new Date('2030-6-21 12:00:00'); - const evtName='dusk'; + const evtName = 'dusk'; const time = suncalc.getTimes(dat, kcLat, kcLon)[evtName]; time.setSeconds(-3); console.log('Wait ...'); @@ -43,10 +50,14 @@ describe('Test Scheduler', function () { console.log(new Date()); const s = new Scheduler(null, Date, suncalc, kcLat, kcLon); - s.add('{"time":{"exactTime":true,"start":"'+evtName.toUpperCase()+'"},"period":{"days":1}}', 'someName2', () => { - console.log(new Date()); - done(); - }); + s.add( + '{"time":{"exactTime":true,"start":"' + evtName.toUpperCase() + '"},"period":{"days":1}}', + 'someName2', + () => { + console.log(new Date()); + done(); + }, + ); }).timeout(65000); it('Test Scheduler: Should not trigger on wrong name', function (done) { @@ -61,9 +72,13 @@ describe('Test Scheduler', function () { console.log(new Date()); const s = new Scheduler(null, Date, suncalc, kcLat, kcLon); - s.add('{"time":{"exactTime":true,"start":"'+evtName.toUpperCase()+'x"},"period":{"days":1}}', 'someName3', () => { - expect(false).to.be.true; - }); + s.add( + '{"time":{"exactTime":true,"start":"' + evtName.toUpperCase() + 'x"},"period":{"days":1}}', + 'someName3', + () => { + expect(false).to.be.true; + }, + ); setTimeout(done, 5000); }).timeout(65000); @@ -84,4 +99,4 @@ describe('Test Scheduler', function () { }); setTimeout(done, 5000); }).timeout(65000); -}); \ No newline at end of file +}); diff --git a/test/testTypeScript.js b/test/testTypeScript.js index 1e29af387..84fec5636 100644 --- a/test/testTypeScript.js +++ b/test/testTypeScript.js @@ -1,8 +1,8 @@ const tsc = require('virtual-tsc'); -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs'); +const path = require('node:path'); +const { EOL } = require('node:os'); const { tsCompilerOptions } = require('../lib/typescriptSettings'); -const { EOL } = require('os'); const { expect } = require('chai'); const { @@ -54,7 +54,9 @@ class Foo { import * as fs from "fs"; export class Foo { do() { } -}`.trim().replace(/\r?\n/g, EOL); +}` + .trim() + .replace(/\r?\n/g, EOL); const transformed = transformScriptBeforeCompilation(source, true); expect(transformed.trim()).to.equal(expected); }); @@ -71,7 +73,9 @@ declare global { getWeekYear(): number; } } -export {};`.trim().replace(/\r?\n/g, EOL); +export {};` + .trim() + .replace(/\r?\n/g, EOL); const transformed = transformScriptBeforeCompilation(source, true); expect(transformed.trim()).to.equal(expected); }); @@ -91,7 +95,9 @@ declare global { export class Foo { do(): void; } -}`.trim().replace(/\r?\n/g, EOL); +}` + .trim() + .replace(/\r?\n/g, EOL); const transformed = transformGlobalDeclarations(source); expect(transformed.trim()).to.equal(expected); }); @@ -108,7 +114,9 @@ declare global { } } export {}; -`.trim().replace(/\r?\n/g, EOL); +` + .trim() + .replace(/\r?\n/g, EOL); const transformed = transformGlobalDeclarations(source); expect(transformed.trim()).to.equal(expected); }); @@ -126,7 +134,9 @@ declare global { getWeekYear(): number; } } -export {};`.trim().replace(/\r?\n/g, EOL); +export {};` + .trim() + .replace(/\r?\n/g, EOL); const transformed = transformGlobalDeclarations(source); expect(transformed.trim()).to.equal(expected); }); @@ -134,9 +144,7 @@ export {};`.trim().replace(/\r?\n/g, EOL); describe('scriptIdToTSFilename', () => { it('generates a valid filename from a script ID', () => { - expect(scriptIdToTSFilename('script.js.foo.bar.baz')).to.equal( - 'foo/bar/baz.ts' - ); + expect(scriptIdToTSFilename('script.js.foo.bar.baz')).to.equal('foo/bar/baz.ts'); }); }); }); @@ -144,11 +152,8 @@ export {};`.trim().replace(/\r?\n/g, EOL); describe('TypeScript compilation regression tests (non-global scripts)', () => { const tsServer = new tsc.Server(tsCompilerOptions, undefined); const tsAmbient = { - 'javascript.d.ts': fs.readFileSync( - path.join(__dirname, '../lib/javascript.d.ts'), - 'utf8' - ), - 'fs.d.ts': `declare module "fs" { }` + 'javascript.d.ts': fs.readFileSync(path.join(__dirname, '../lib/javascript.d.ts'), 'utf8'), + 'fs.d.ts': `declare module "fs" { }`, }; tsServer.provideAmbientDeclarations(tsAmbient); @@ -199,7 +204,7 @@ async function bar():Promise { } await bar(); -` +`, ]; for (let i = 0; i < tests.length; i++) { @@ -217,10 +222,7 @@ await bar(); describe('TypeScript compilation regression tests (global scripts)', () => { const tsServer = new tsc.Server(tsCompilerOptions, undefined); const tsAmbient = { - 'javascript.d.ts': fs.readFileSync( - path.join(__dirname, '../lib/javascript.d.ts'), - 'utf8' - ), + 'javascript.d.ts': fs.readFileSync(path.join(__dirname, '../lib/javascript.d.ts'), 'utf8'), 'fs.d.ts': `declare module "fs" { }`, 'otherglobal.d.ts': ` declare global { @@ -229,7 +231,7 @@ declare global { } }; export {}; -` +`, }; tsServer.provideAmbientDeclarations(tsAmbient); @@ -264,7 +266,7 @@ d.getWeekYear() function test(): void { log("test global"); } -` +`, ]; for (let i = 0; i < tests.length; i++) { diff --git a/tsconfig.json b/tsconfig.json index 5390f927c..70f07ce50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,9 +23,8 @@ // "noUnusedLocals": true, // "noUnusedParameters": true, "target": "es2022", + "esModuleInterop": true, "types": ["node", "@iobroker/types"], }, - "include": ["lib/**/*.js", "lib/**/*.d.ts", - "src/main" - ] + "include": ["src/**/*.ts"] }