From 3794a13a7cb81d59546e5f279db8947563a46f53 Mon Sep 17 00:00:00 2001 From: Acgua <95978245+Acgua@users.noreply.github.com> Date: Thu, 30 Mar 2023 20:18:47 +0200 Subject: [PATCH] Update ... --- build/lib/interfaces.js.map | 2 +- build/lib/methods.js.map | 2 +- build/lib/mqtt-server.js.map | 2 +- build/lib/restApi.js.map | 2 +- build/main.js.map | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build/lib/interfaces.js.map b/build/lib/interfaces.js.map index 0d0e4bd..f5c15ef 100644 --- a/build/lib/interfaces.js.map +++ b/build/lib/interfaces.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../src/lib/interfaces.ts"], - "sourcesContent": ["export interface IDevice {\n name: string; // e.g. \"Tablet Hallway Entry\"\n id: string; // e.g. \"Tablet-Hallway-Entry\" (meets ioBroker state convention)\n ip: string;\n enabled: true | false;\n restProtocol: 'http' | 'https';\n restPort: number;\n restPassword: string;\n lastSeen: number; // timestamp\n isAlive: true | false;\n mqttInfoObjectsCreated: true | false; // Set to true once first time creation initiated\n mqttInfoKeys: string[]; // Info keys from MQTT info, like 'batteryLevel', 'deviceID', ...\n}\n\nexport interface ICmds {\n readonly id: string;\n readonly name: string;\n readonly type: 'number' | 'boolean' | 'string';\n readonly cmdOn?: string;\n readonly cmdOff?: string;\n readonly mqttOn?: string;\n readonly mqttOff?: string;\n}\n\nexport interface IMqttDevice {\n ip?: string;\n lastTimeActive?: number;\n mqttFirstReceived?: true | false;\n isActive?: true | false;\n timeoutNoUpdate?: ioBroker.Timeout | null;\n previousInfoPublishTime?: number;\n}\n\nexport interface IConst {\n readonly mqttEvents: string[];\n readonly cmds: ICmds[];\n readonly cmdsSwitches: ICmds[];\n}\n"], + "sourcesContent": ["export interface IDevice {\r\n name: string; // e.g. \"Tablet Hallway Entry\"\r\n id: string; // e.g. \"Tablet-Hallway-Entry\" (meets ioBroker state convention)\r\n ip: string;\r\n enabled: true | false;\r\n restProtocol: 'http' | 'https';\r\n restPort: number;\r\n restPassword: string;\r\n lastSeen: number; // timestamp\r\n isAlive: true | false;\r\n mqttInfoObjectsCreated: true | false; // Set to true once first time creation initiated\r\n mqttInfoKeys: string[]; // Info keys from MQTT info, like 'batteryLevel', 'deviceID', ...\r\n}\r\n\r\nexport interface ICmds {\r\n readonly id: string;\r\n readonly name: string;\r\n readonly type: 'number' | 'boolean' | 'string';\r\n readonly cmdOn?: string;\r\n readonly cmdOff?: string;\r\n readonly mqttOn?: string;\r\n readonly mqttOff?: string;\r\n}\r\n\r\nexport interface IMqttDevice {\r\n ip?: string;\r\n lastTimeActive?: number;\r\n mqttFirstReceived?: true | false;\r\n isActive?: true | false;\r\n timeoutNoUpdate?: ioBroker.Timeout | null;\r\n previousInfoPublishTime?: number;\r\n}\r\n\r\nexport interface IConst {\r\n readonly mqttEvents: string[];\r\n readonly cmds: ICmds[];\r\n readonly cmdsSwitches: ICmds[];\r\n}\r\n"], "mappings": ";;;;;;;;;;;;;AAAA;AAAA;", "names": [] } diff --git a/build/lib/methods.js.map b/build/lib/methods.js.map index 6bf57c2..a92be12 100644 --- a/build/lib/methods.js.map +++ b/build/lib/methods.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../src/lib/methods.ts"], - "sourcesContent": ["/**\n * Methods and Tools\n */\n\nimport { FullyMqtt } from '../main';\n\n/**\n * Convert error to string\n * @param {*} error - any kind of thrown error\n * @returns string\n */\nexport function err2Str(error: any): string {\n if (error instanceof Error) {\n if (error.stack) return error.stack;\n if (error.message) return error.message;\n return JSON.stringify(error);\n } else {\n if (typeof error === 'string') return error;\n return JSON.stringify(error);\n }\n}\n\n/**\n * Clean device name for state\n * @param str - device name\n * @returns device name without forbidden chars, and without any dots.\n */\nexport function cleanDeviceName(this: FullyMqtt, str: string): string {\n let res = str.replace(this.FORBIDDEN_CHARS, ''); // https://github.com/ioBroker/ioBroker.js-controller/blob/master/packages/common/src/lib/common/tools.ts\n res = res.replace(/\\./g, ''); // remove any dots \".\"\n res = res.replace(/\\s{2,}/g, ' '); // replace multiple whitespaces with single space\n res = res.trim(); // removes whitespace from both ends\n res = res.replace(/\\s/g, '_'); // replace whitespaces with _\n if (res.replace(/_/g, '').length === 0) res = ''; // return empty str if just _ is left\n return res;\n}\n\n/**\n * Check if IP address is valid - https://stackoverflow.com/a/27434991\n * @param ip IP address\n * @returns true if valid, false if not\n */\nexport function isIpAddressValid(ip: string): true | false {\n const pattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;\n if (pattern.test(ip)) {\n return true;\n } else {\n return false;\n }\n}\n\n/**\n * Retrieve values from a CONFIG variable, example:\n * const CONF = [{car: 'bmw', color: 'black', hp: '250'}, {car: 'audi', color: 'blue', hp: '190'}]\n * To get the color of the Audi, use: getConfigValuePerKey(CONF, 'car', 'audi', 'color')\n * To find out which car has 190 hp, use: getConfigValuePerKey(CONF, 'hp', '190', 'car')\n * @param {object} config The configuration variable/constant\n * @param {string} key1 Key to look for.\n * @param {string | number} key1Value The value the key should have\n * @param {string} key2 The key which value we return\n * @returns {any} Returns the element's value, or number -1 of nothing found.\n */\nexport function getConfigValuePerKey(config: { [k: string]: any }[], key1: string, key1Value: string | number, key2: string): any {\n for (const lpConfDevice of config) {\n if (lpConfDevice[key1] === key1Value) {\n if (lpConfDevice[key2] === undefined) {\n return -1;\n } else {\n return lpConfDevice[key2];\n }\n }\n }\n return -1;\n}\n\n/**\n * Checks if an operand (variable, constant, object, ...) is considered as empty.\n * - empty: undefined; null; string|array|object, stringified and only with white space(s), and/or `><[]{}`\n * - NOT empty: not matching anything above; any function; boolean false; number -1\n * inspired by helper.js from SmartControl adapter\n */\nexport function isEmpty(toCheck: any): true | false {\n if (toCheck === null || typeof toCheck === 'undefined') return true;\n if (typeof toCheck === 'function') return false;\n let x = JSON.stringify(toCheck);\n x = x.replace(/\\s+/g, ''); // white space(s)\n x = x.replace(/\"+/g, ''); // \"\n x = x.replace(/'+/g, ''); // '\n x = x.replace(/\\[+/g, ''); // [\n x = x.replace(/\\]+/g, ''); // ]\n x = x.replace(/\\{+/g, ''); // {\n x = x.replace(/\\}+/g, ''); // }\n return x === '' ? true : false;\n}\n\n/**\n * async wait/pause\n * Actually not needed since a single line, but for the sake of using wait more easily\n * @param {number} ms - number of milliseconds to wait\n */\nexport async function wait(this: FullyMqtt, ms: number): Promise {\n try {\n await new Promise((w) => setTimeout(w, ms));\n } catch (e) {\n this.log.error(this.err2Str(e));\n return;\n }\n}\n"], + "sourcesContent": ["/**\r\n * Methods and Tools\r\n */\r\n\r\nimport { FullyMqtt } from '../main';\r\n\r\n/**\r\n * Convert error to string\r\n * @param {*} error - any kind of thrown error\r\n * @returns string\r\n */\r\nexport function err2Str(error: any): string {\r\n if (error instanceof Error) {\r\n if (error.stack) return error.stack;\r\n if (error.message) return error.message;\r\n return JSON.stringify(error);\r\n } else {\r\n if (typeof error === 'string') return error;\r\n return JSON.stringify(error);\r\n }\r\n}\r\n\r\n/**\r\n * Clean device name for state\r\n * @param str - device name\r\n * @returns device name without forbidden chars, and without any dots.\r\n */\r\nexport function cleanDeviceName(this: FullyMqtt, str: string): string {\r\n let res = str.replace(this.FORBIDDEN_CHARS, ''); // https://github.com/ioBroker/ioBroker.js-controller/blob/master/packages/common/src/lib/common/tools.ts\r\n res = res.replace(/\\./g, ''); // remove any dots \".\"\r\n res = res.replace(/\\s{2,}/g, ' '); // replace multiple whitespaces with single space\r\n res = res.trim(); // removes whitespace from both ends\r\n res = res.replace(/\\s/g, '_'); // replace whitespaces with _\r\n if (res.replace(/_/g, '').length === 0) res = ''; // return empty str if just _ is left\r\n return res;\r\n}\r\n\r\n/**\r\n * Check if IP address is valid - https://stackoverflow.com/a/27434991\r\n * @param ip IP address\r\n * @returns true if valid, false if not\r\n */\r\nexport function isIpAddressValid(ip: string): true | false {\r\n const pattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;\r\n if (pattern.test(ip)) {\r\n return true;\r\n } else {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Retrieve values from a CONFIG variable, example:\r\n * const CONF = [{car: 'bmw', color: 'black', hp: '250'}, {car: 'audi', color: 'blue', hp: '190'}]\r\n * To get the color of the Audi, use: getConfigValuePerKey(CONF, 'car', 'audi', 'color')\r\n * To find out which car has 190 hp, use: getConfigValuePerKey(CONF, 'hp', '190', 'car')\r\n * @param {object} config The configuration variable/constant\r\n * @param {string} key1 Key to look for.\r\n * @param {string | number} key1Value The value the key should have\r\n * @param {string} key2 The key which value we return\r\n * @returns {any} Returns the element's value, or number -1 of nothing found.\r\n */\r\nexport function getConfigValuePerKey(config: { [k: string]: any }[], key1: string, key1Value: string | number, key2: string): any {\r\n for (const lpConfDevice of config) {\r\n if (lpConfDevice[key1] === key1Value) {\r\n if (lpConfDevice[key2] === undefined) {\r\n return -1;\r\n } else {\r\n return lpConfDevice[key2];\r\n }\r\n }\r\n }\r\n return -1;\r\n}\r\n\r\n/**\r\n * Checks if an operand (variable, constant, object, ...) is considered as empty.\r\n * - empty: undefined; null; string|array|object, stringified and only with white space(s), and/or `><[]{}`\r\n * - NOT empty: not matching anything above; any function; boolean false; number -1\r\n * inspired by helper.js from SmartControl adapter\r\n */\r\nexport function isEmpty(toCheck: any): true | false {\r\n if (toCheck === null || typeof toCheck === 'undefined') return true;\r\n if (typeof toCheck === 'function') return false;\r\n let x = JSON.stringify(toCheck);\r\n x = x.replace(/\\s+/g, ''); // white space(s)\r\n x = x.replace(/\"+/g, ''); // \"\r\n x = x.replace(/'+/g, ''); // '\r\n x = x.replace(/\\[+/g, ''); // [\r\n x = x.replace(/\\]+/g, ''); // ]\r\n x = x.replace(/\\{+/g, ''); // {\r\n x = x.replace(/\\}+/g, ''); // }\r\n return x === '' ? true : false;\r\n}\r\n\r\n/**\r\n * async wait/pause\r\n * Actually not needed since a single line, but for the sake of using wait more easily\r\n * @param {number} ms - number of milliseconds to wait\r\n */\r\nexport async function wait(this: FullyMqtt, ms: number): Promise {\r\n try {\r\n await new Promise((w) => setTimeout(w, ms));\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return;\r\n }\r\n}\r\n"], "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWO,SAAS,QAAQ,OAAoB;AACxC,MAAI,iBAAiB,OAAO;AACxB,QAAI,MAAM;AAAO,aAAO,MAAM;AAC9B,QAAI,MAAM;AAAS,aAAO,MAAM;AAChC,WAAO,KAAK,UAAU,KAAK;AAAA,EAC/B,OAAO;AACH,QAAI,OAAO,UAAU;AAAU,aAAO;AACtC,WAAO,KAAK,UAAU,KAAK;AAAA,EAC/B;AACJ;AAOO,SAAS,gBAAiC,KAAqB;AAClE,MAAI,MAAM,IAAI,QAAQ,KAAK,iBAAiB,EAAE;AAC9C,QAAM,IAAI,QAAQ,OAAO,EAAE;AAC3B,QAAM,IAAI,QAAQ,WAAW,GAAG;AAChC,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,QAAQ,OAAO,GAAG;AAC5B,MAAI,IAAI,QAAQ,MAAM,EAAE,EAAE,WAAW;AAAG,UAAM;AAC9C,SAAO;AACX;AAOO,SAAS,iBAAiB,IAA0B;AACvD,QAAM,UAAU;AAChB,MAAI,QAAQ,KAAK,EAAE,GAAG;AAClB,WAAO;AAAA,EACX,OAAO;AACH,WAAO;AAAA,EACX;AACJ;AAaO,SAAS,qBAAqB,QAAgC,MAAc,WAA4B,MAAmB;AAC9H,aAAW,gBAAgB,QAAQ;AAC/B,QAAI,aAAa,UAAU,WAAW;AAClC,UAAI,aAAa,UAAU,QAAW;AAClC,eAAO;AAAA,MACX,OAAO;AACH,eAAO,aAAa;AAAA,MACxB;AAAA,IACJ;AAAA,EACJ;AACA,SAAO;AACX;AAQO,SAAS,QAAQ,SAA4B;AAChD,MAAI,YAAY,QAAQ,OAAO,YAAY;AAAa,WAAO;AAC/D,MAAI,OAAO,YAAY;AAAY,WAAO;AAC1C,MAAI,IAAI,KAAK,UAAU,OAAO;AAC9B,MAAI,EAAE,QAAQ,QAAQ,EAAE;AACxB,MAAI,EAAE,QAAQ,OAAO,EAAE;AACvB,MAAI,EAAE,QAAQ,OAAO,EAAE;AACvB,MAAI,EAAE,QAAQ,QAAQ,EAAE;AACxB,MAAI,EAAE,QAAQ,QAAQ,EAAE;AACxB,MAAI,EAAE,QAAQ,QAAQ,EAAE;AACxB,MAAI,EAAE,QAAQ,QAAQ,EAAE;AACxB,SAAO,MAAM,KAAK,OAAO;AAC7B;AAOA,eAAsB,KAAsB,IAA2B;AACnE,MAAI;AACA,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAAA,EAC9C,SAAS,GAAP;AACE,SAAK,IAAI,MAAM,KAAK,QAAQ,CAAC,CAAC;AAC9B;AAAA,EACJ;AACJ;", "names": [] } diff --git a/build/lib/mqtt-server.js.map b/build/lib/mqtt-server.js.map index 770bad5..1195c91 100644 --- a/build/lib/mqtt-server.js.map +++ b/build/lib/mqtt-server.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../src/lib/mqtt-server.ts"], - "sourcesContent": ["import Aedes from 'aedes';\nimport net from 'net';\nimport { FullyMqtt } from '../main';\nimport { IMqttDevice } from './interfaces';\n//import { inspect } from 'util';\n\nexport class MqttServer {\n private readonly adapter: FullyMqtt;\n private server: net.Server;\n private aedes: Aedes;\n public devices: { [mqttClientId: string]: IMqttDevice }; // {}\n private port = -1;\n private notAuthorizedClients: string[] = []; // to avoid multiple log lines\n\n /**\n * Constructor\n */\n public constructor(adapter: FullyMqtt) {\n this.adapter = adapter;\n //this.server = new net.Server();\n this.aedes = new Aedes();\n /** @ts-expect-error - https://github.com/moscajs/aedes/issues/801 */\n this.server = net.createServer(undefined, this.aedes.handle);\n this.devices = {}; // key = MQTT Client ID, property: IMqttDevice\n }\n\n /**\n * Listen\n */\n public start(): void {\n try {\n /**\n * Port\n */\n this.port = this.adapter.config.mqttPort;\n /**\n * #############################################################\n * For Developer only: change port if in dev environment\n * #############################################################\n */\n if (this.adapter.adapterDir.includes('/.dev-server/default/node_modules')) {\n this.port = 3012;\n this.adapter.log.warn(`DEVELOPER: Port changed to ${this.port} as we are in DEV Environment! If you see this log message, please open an issue on Github.`);\n }\n\n /**\n * Start Listening\n */\n this.server.listen(this.port, () => {\n this.adapter.log.info(`\uD83D\uDE80 MQTT Server started and is listening on port ${this.port}.`);\n });\n\n /**\n * Verify authorization\n * This fires first and before this.aedes.on('client', (client) ...\n * https://github.com/moscajs/aedes/blob/main/docs/Aedes.md#handler-authenticate-client-username-password-callback\n */\n this.aedes.authenticate = (client, username, password, callback) => {\n try {\n // If we saw client before and is not authorized\n if (this.notAuthorizedClients.includes(client.id)) {\n callback(null, false);\n return;\n }\n\n // Create device entry with id as key, if not yet existing\n if (!this.devices[client.id]) this.devices[client.id] = {};\n\n /**\n * Get IP\n * This rather complicated way is needed, see https://github.com/moscajs/aedes/issues/186\n * Not sure if this always works, but client.req was undefined in my test - which is suggested in https://github.com/moscajs/aedes/issues/527\n */\n let ip: string | undefined = undefined;\n if (client.conn && 'remoteAddress' in client.conn && typeof client.conn.remoteAddress === 'string') {\n const ipSource = client.conn.remoteAddress; // like: ::ffff:192.168.10.101\n this.adapter.log.debug(`[MQTT] client.conn.remoteAddress = \"${ipSource}\" - ${client.id}`);\n ip = ipSource.substring(ipSource.lastIndexOf(':') + 1); // get everything after last \":\"\n if (!this.adapter.isIpAddressValid(ip)) ip === undefined;\n }\n // Check if IP is an active device IP\n if (ip && !Object.keys(this.adapter.fullysEnbl).includes(ip)) {\n this.adapter.log.error(`[MQTT] Client ${client.id} not authorized: ${ip} is not an active Fully device IP per adapter settings.`);\n this.notAuthorizedClients.push(client.id);\n callback(null, false);\n return;\n }\n\n const ipMsg = ip ? `${this.adapter.fullysEnbl[ip].name} (${ip})` : `${client.id} (IP unknown)`;\n this.adapter.log.debug(`[MQTT] Client ${ipMsg} trys to authenticate...`);\n if (ip) this.devices[client.id].ip = ip;\n\n /**\n * Verify User and Password\n */\n if (!this.adapter.config.mqttDoNotVerifyUserPw) {\n // Username\n if (username !== this.adapter.config.mqttUser) {\n this.adapter.log.warn(`MQTT Client ${ipMsg} Authorization rejected: received user name '${username}' does not match '${this.adapter.config.mqttUser}' in adapter settings.`);\n callback(null, false);\n return;\n }\n // Password\n if (password.toString() !== this.adapter.config.mqttPassword) {\n this.adapter.log.warn(`MQTT Client ${ipMsg} Authorization rejected: received password does not match with password in adapter settings.`);\n callback(null, false);\n return;\n }\n }\n this.adapter.log.info(`\uD83D\uDD11 MQTT Client ${ipMsg} successfully authenticated.`);\n callback(null, true);\n } catch (e) {\n this.adapter.log.error(this.adapter.err2Str(e));\n callback(null, false);\n }\n };\n\n /**\n * fired when a client connects\n */\n this.aedes.on('client', (client) => {\n try {\n if (!client) return;\n\n // Create device entry with id as key, if not yet existing (should have been set in this.aedes.authenticate already)\n if (!this.devices[client.id]) this.devices[client.id] = {};\n\n // IP\n const ip = this.devices[client.id].ip;\n const ipMsg = ip ? `${this.adapter.fullysEnbl[ip].name} (${ip})` : `${client.id} (IP unknown)`;\n\n this.adapter.log.debug(`[MQTT] Client ${ipMsg} connected to broker ${this.aedes.id}`);\n this.adapter.log.info(`\uD83D\uDD17 MQTT Client ${ipMsg} successfully connected.`);\n //this.adapter.log.debug(inspect(client)); //https://stackoverflow.com/a/31557814\n\n // set isAlive\n this.setIsAlive(client.id, true, 'client connected');\n\n // Schedule check if still alive\n this.scheduleCheckIfStillActive(client.id);\n } catch (e) {\n this.adapter.log.error(this.adapter.err2Str(e));\n return;\n }\n });\n\n /**\n * fired when a client publishes a message packet on the topic\n */\n this.aedes.on('publish', (packet, client) => {\n try {\n if (!client || !packet) return;\n\n this.setIsAlive(client.id, true, 'client published message');\n\n // Create device entry with id as key, if not yet existing\n if (!this.devices[client.id]) this.devices[client.id] = {};\n\n // QOS is always 1 per Fully documentation\n if (packet.qos !== 1) return;\n\n if (packet.retain) {\n /**\n * Device Info coming in...\n * Per fully documentation: The complete device info will be published every 60 seconds as fully/deviceInfo/[deviceId] topic (retaining, QOS=1).\n */\n\n // Payload as object\n const info = JSON.parse(packet.payload.toString());\n\n // Verification of device info packet\n // We don't use topic to check since we do not want to rely on user's input in Fully Browser \"MQTT Device Info Topic\" settings.\n if (!('startUrl' in info) && !('ip4' in info)) {\n this.adapter.log.error(`[MQTT] Packet rejected: ${info.ip4} - Info packet expected, but ip4 and startUrl is not defined in packet. ${info.deviceId}`);\n return;\n }\n\n // IP\n const ip = info.ip4;\n const devMsg = `${this.adapter.fullysEnbl[ip].name} (${ip})`;\n // Check IP - already done in this.aedes.authenticate, but just in case we were unable to get ip there\n if (!Object.keys(this.adapter.fullysEnbl).includes(ip)) {\n this.adapter.log.error(`[MQTT] Client ${devMsg} Packet rejected: IP is not allowed per adapter settings. ${client.id}`);\n return;\n }\n this.devices[client.id].ip = ip;\n\n // Slow down: Don't accept info event more often than x seconds\n // Per Fully doc, should not come in more often than 60s anyway...\n const prevTime = this.devices[client.id].previousInfoPublishTime;\n const limit = this.adapter.config.mqttPublishedInfoDelay * 1000; // milliseconds\n if (prevTime && prevTime !== 0) {\n if (Date.now() - prevTime < limit) {\n const diffMs = Date.now() - prevTime;\n this.adapter.log.silly(`[MQTT] ${devMsg} Packet rejected: Last packet came in ${diffMs}ms (${Math.round(diffMs / 1000)}s) ago...`);\n return;\n }\n }\n this.devices[client.id].previousInfoPublishTime = Date.now(); // set for future events\n\n /**\n * First time received device info incl. IP address etc.\n */\n if (!this.devices[client.id].mqttFirstReceived) {\n // show only once\n this.adapter.log.debug(`[MQTT] Client ${client.id} = ${this.adapter.fullysEnbl[ip].name} = ${ip}`);\n // set to true\n this.devices[client.id].mqttFirstReceived = true;\n }\n /**\n * Call Adapter function onMqttInfo()\n */\n const result = {\n clientId: client.id,\n ip: ip,\n topic: packet.topic,\n infoObj: info,\n };\n this.adapter.onMqttInfo(result);\n } else if (packet.qos === 1 && !packet.retain) {\n /**\n * Event coming in...\n * Per fully documentation: Events will be published as fully/event/[eventId]/[deviceId] topic (non-retaining, QOS=1).\n */\n // {\"deviceId\":\"xxxxxxxx-xxxxxxxx\",\"event\":\"screenOn\"}\n // NOTE: Device ID is different to client id, we actually disregard deviceId\n const msg = JSON.parse(packet.payload.toString());\n\n // Verification of event packet\n // We don't use topic to check since we do not want to rely on user's input in Fully Browser \"MQTT Event Topic\" settings.\n if (!('event' in msg)) {\n this.adapter.log.error(`[MQTT] Packet rejected: Event packet expected, but event is not defined in packet. ${client.id}`);\n return;\n }\n\n // Disregard first event once connected: mqttConnected\n if (msg.event === 'mqttConnected') {\n this.adapter.log.silly(`[MQTT] Client Publish Event: Disregard mqttConnected event - ${msg.deviceId}`);\n return;\n }\n\n // Get IP\n if (!this.devices[client.id]) {\n this.adapter.log.info(`[MQTT] Client Publish Event: Device ID and according IP not yet seen thru \"Publish Info\"`);\n this.adapter.log.info(`[MQTT] We wait until first info is published. ${msg.deviceId}`);\n return;\n }\n const ip = this.devices[client.id].ip ? this.devices[client.id].ip : '';\n if (ip === '' || typeof ip !== 'string') {\n this.adapter.log.debug(`[MQTT] Client Publish Event: IP address could not be determined. - Client ID: ${client.id}`);\n this.adapter.log.debug(`[MQTT] Please be patient until first MQTT info packet coming in (takes up to 1 minute)`);\n return; // Disregard since IP is unknown!\n }\n\n // Call function\n const result = {\n clientId: client.id,\n ip: ip,\n topic: packet.topic,\n cmd: msg.event,\n };\n if (!this.devices[client.id].mqttFirstReceived) {\n // show only once\n this.adapter.log.info(`[MQTT] \uD83D\uDD17 Client ${client.id} = ${this.adapter.fullysEnbl[ip].name} (${ip})`);\n this.devices[client.id].mqttFirstReceived = true;\n }\n /**\n * Call Adapter function onMqttEvent()\n */\n this.adapter.onMqttEvent(result);\n } else {\n // Ignore\n return;\n }\n } catch (e) {\n this.adapter.log.error(this.adapter.err2Str(e));\n return;\n }\n });\n\n /**\n * fired when a client disconnects\n */\n this.aedes.on('clientDisconnect', (client) => {\n const ip = this.devices[client.id].ip;\n const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id;\n if (this.adapter.config.mqttConnErrorsAsInfo) {\n this.adapter.log.info(`MQTT Client ${logMsgName} disconnected.`);\n } else {\n this.adapter.log.error(`[MQTT] Client ${logMsgName} disconnected.`);\n }\n this.setIsAlive(client.id, false, 'client disconnected');\n });\n\n /**\n * fired on client error\n */\n this.aedes.on('clientError', (client, e) => {\n if (this.notAuthorizedClients.includes(client.id)) return; // Error msg was already thrown in aedes.authenticate() before\n const ip = this.devices[client.id].ip;\n const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id;\n if (this.adapter.config.mqttConnErrorsAsInfo) {\n this.adapter.log.info(`[MQTT] ${logMsgName}: Client error - ${e.message}`);\n } else {\n this.adapter.log.error(`[MQTT]\uD83D\uDD25 ${logMsgName}: Client error - ${e.message}`);\n }\n this.adapter.log.debug(`[MQTT]\uD83D\uDD25 ${logMsgName}: Client error - stack: ${e.stack}`);\n this.setIsAlive(client.id, false, 'client error');\n });\n\n this.aedes.on('connectionError', (client, e) => {\n const ip = this.devices[client.id].ip;\n const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id;\n if (this.adapter.config.mqttConnErrorsAsInfo) {\n this.adapter.log.info(`[MQTT] ${logMsgName}: Connection error - ${e.message}`);\n } else {\n this.adapter.log.error(`[MQTT]\uD83D\uDD25 ${logMsgName}: Connection error - ${e.message}`);\n }\n this.adapter.log.debug(`[MQTT]\uD83D\uDD25 ${logMsgName}: Connection error - stack: ${e.stack}`);\n this.setIsAlive(client.id, false, 'connection error');\n });\n\n /**\n * fired on server error\n */\n this.server.on('error', (e: any) => {\n if (e instanceof Error && e.message.startsWith('listen EADDRINUSE')) {\n this.adapter.log.debug(`[MQTT] Cannot start server - ${e.message}`);\n this.adapter.log.error(`[MQTT]\uD83D\uDD25 Cannot start server - Port ${this.port} is already in use. Try a different port!`);\n } else {\n this.adapter.log.error(`[MQTT]\uD83D\uDD25 Cannot start server - ${e.message}`);\n }\n this.terminate();\n });\n } catch (e) {\n this.adapter.log.error(this.adapter.err2Str(e));\n return;\n }\n }\n\n /**\n * If Client is alive or not\n */\n private setIsAlive(clientId: string, isAlive: true | false, msg: string): void {\n if (isAlive) this.devices[clientId].lastTimeActive = Date.now();\n this.devices[clientId].isActive = isAlive;\n\n const ip = this.devices[clientId]?.ip;\n if (ip) {\n // Call Adapter function onMqttAliveChange()\n this.adapter.onMqttAlive(ip, isAlive, msg);\n if (isAlive) {\n this.scheduleCheckIfStillActive(clientId); // restart timer\n } else {\n // clear timer\n // @ts-expect-error \"Type 'null' is not assignable to type 'Timeout'.ts(2345)\" - we check for not being null via \"if\"\n if (this.devices[clientId].timeoutNoUpdate) this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate);\n }\n } else {\n this.adapter.log.debug(`[MQTT] isAlive changed to ${isAlive}, but IP of client ${clientId} is still unknown.`);\n }\n }\n\n /**\n * Schedule: Check if MQTT topic was sent last x seconds ago\n * @param ip IP Address\n * @returns void\n */\n private async scheduleCheckIfStillActive(clientId: string): Promise {\n try {\n const ip = this.devices[clientId].ip;\n const ipMsg = ip ? `${this.adapter.fullysEnbl[ip].name} (${ip})` : `${clientId} (IP unknown)`;\n // this.adapter.log.debug(`[MQTT] ${ipMsg}: - Start scheduleCheckIfStillActive`);\n\n // @ts-expect-error \"Type 'null' is not assignable to type 'Timeout'.ts(2345)\" - we check for not being null via \"if\"\n if (this.devices[clientId].timeoutNoUpdate) this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate);\n\n if (!this.devices[clientId]) this.devices[clientId] = {};\n\n const interval = 70 * 1000; // every 60s + 10s buffer\n this.devices[clientId].timeoutNoUpdate = this.adapter.setTimeout(async () => {\n try {\n const lastTimeActive = this.devices[clientId].lastTimeActive;\n if (!lastTimeActive) return;\n const diff = Date.now() - lastTimeActive;\n if (diff > 70000) {\n this.adapter.log.debug(`[MQTT] ${ipMsg} NOT ALIVE - last contact ${Math.round(diff / 1000)}s (${diff}ms) ago`);\n this.setIsAlive(clientId, false, 'client did not send message for more than 70 seconds');\n } else {\n this.adapter.log.warn(`[MQTT] ${ipMsg} Please open a issue on Github, this should never happen: scheduleCheckIfStillActive() timeout, and last contact was less than 70s ago.`);\n this.adapter.log.warn(`[MQTT] ${ipMsg} is alive - last contact ${Math.round(diff / 1000)}s (${diff}ms) ago`);\n this.setIsAlive(clientId, true, `alive check is successful (last contact: ${Math.round(diff / 1000)}s ago)`);\n }\n // Call function again since we are in callback of timeout\n this.scheduleCheckIfStillActive(clientId);\n } catch (e) {\n this.adapter.log.error(this.adapter.err2Str(e));\n return;\n }\n }, interval);\n } catch (e) {\n this.adapter.log.error(this.adapter.err2Str(e));\n return;\n }\n }\n\n /**\n * Terminate MQTT Server and close all...\n */\n public terminate(): void {\n this.adapter.log.info(`[MQTT] Disconnect all clients and close server`);\n // isAlive\n for (const clientId in this.devices) {\n // @ts-expect-error \"Type 'null' is not assignable to type 'Timeout'.ts(2345)\" - we check for not being null via \"if\"\n if (this.devices[clientId].timeoutNoUpdate) this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate);\n this.setIsAlive(clientId, false, 'MQTT server was terminated');\n }\n\n if (this.aedes) {\n this.aedes.close(() => {\n this.adapter.log.debug('[MQTT] aedes.close() succeeded');\n if (this.server) {\n this.server.close(() => {\n this.adapter.log.debug('[MQTT] server.close() succeeded');\n });\n }\n });\n } else if (this.server) {\n this.server.close(() => {\n this.adapter.log.debug('[MQTT] server.close() succeeded');\n });\n }\n }\n}\n"], + "sourcesContent": ["import Aedes from 'aedes';\r\nimport net from 'net';\r\nimport { FullyMqtt } from '../main';\r\nimport { IMqttDevice } from './interfaces';\r\n//import { inspect } from 'util';\r\n\r\nexport class MqttServer {\r\n private readonly adapter: FullyMqtt;\r\n private server: net.Server;\r\n private aedes: Aedes;\r\n public devices: { [mqttClientId: string]: IMqttDevice }; // {}\r\n private port = -1;\r\n private notAuthorizedClients: string[] = []; // to avoid multiple log lines\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(adapter: FullyMqtt) {\r\n this.adapter = adapter;\r\n //this.server = new net.Server();\r\n this.aedes = new Aedes();\r\n /** @ts-expect-error - https://github.com/moscajs/aedes/issues/801 */\r\n this.server = net.createServer(undefined, this.aedes.handle);\r\n this.devices = {}; // key = MQTT Client ID, property: IMqttDevice\r\n }\r\n\r\n /**\r\n * Listen\r\n */\r\n public start(): void {\r\n try {\r\n /**\r\n * Port\r\n */\r\n this.port = this.adapter.config.mqttPort;\r\n /**\r\n * #############################################################\r\n * For Developer only: change port if in dev environment\r\n * #############################################################\r\n */\r\n if (this.adapter.adapterDir.includes('/.dev-server/default/node_modules')) {\r\n this.port = 3012;\r\n this.adapter.log.warn(`DEVELOPER: Port changed to ${this.port} as we are in DEV Environment! If you see this log message, please open an issue on Github.`);\r\n }\r\n\r\n /**\r\n * Start Listening\r\n */\r\n this.server.listen(this.port, () => {\r\n this.adapter.log.info(`\uD83D\uDE80 MQTT Server started and is listening on port ${this.port}.`);\r\n });\r\n\r\n /**\r\n * Verify authorization\r\n * This fires first and before this.aedes.on('client', (client) ...\r\n * https://github.com/moscajs/aedes/blob/main/docs/Aedes.md#handler-authenticate-client-username-password-callback\r\n */\r\n this.aedes.authenticate = (client, username, password, callback) => {\r\n try {\r\n // If we saw client before and is not authorized\r\n if (this.notAuthorizedClients.includes(client.id)) {\r\n callback(null, false);\r\n return;\r\n }\r\n\r\n // Create device entry with id as key, if not yet existing\r\n if (!this.devices[client.id]) this.devices[client.id] = {};\r\n\r\n /**\r\n * Get IP\r\n * This rather complicated way is needed, see https://github.com/moscajs/aedes/issues/186\r\n * Not sure if this always works, but client.req was undefined in my test - which is suggested in https://github.com/moscajs/aedes/issues/527\r\n */\r\n let ip: string | undefined = undefined;\r\n if (client.conn && 'remoteAddress' in client.conn && typeof client.conn.remoteAddress === 'string') {\r\n const ipSource = client.conn.remoteAddress; // like: ::ffff:192.168.10.101\r\n this.adapter.log.debug(`[MQTT] client.conn.remoteAddress = \"${ipSource}\" - ${client.id}`);\r\n ip = ipSource.substring(ipSource.lastIndexOf(':') + 1); // get everything after last \":\"\r\n if (!this.adapter.isIpAddressValid(ip)) ip === undefined;\r\n }\r\n // Check if IP is an active device IP\r\n if (ip && !Object.keys(this.adapter.fullysEnbl).includes(ip)) {\r\n this.adapter.log.error(`[MQTT] Client ${client.id} not authorized: ${ip} is not an active Fully device IP per adapter settings.`);\r\n this.notAuthorizedClients.push(client.id);\r\n callback(null, false);\r\n return;\r\n }\r\n\r\n const ipMsg = ip ? `${this.adapter.fullysEnbl[ip].name} (${ip})` : `${client.id} (IP unknown)`;\r\n this.adapter.log.debug(`[MQTT] Client ${ipMsg} trys to authenticate...`);\r\n if (ip) this.devices[client.id].ip = ip;\r\n\r\n /**\r\n * Verify User and Password\r\n */\r\n if (!this.adapter.config.mqttDoNotVerifyUserPw) {\r\n // Username\r\n if (username !== this.adapter.config.mqttUser) {\r\n this.adapter.log.warn(`MQTT Client ${ipMsg} Authorization rejected: received user name '${username}' does not match '${this.adapter.config.mqttUser}' in adapter settings.`);\r\n callback(null, false);\r\n return;\r\n }\r\n // Password\r\n if (password.toString() !== this.adapter.config.mqttPassword) {\r\n this.adapter.log.warn(`MQTT Client ${ipMsg} Authorization rejected: received password does not match with password in adapter settings.`);\r\n callback(null, false);\r\n return;\r\n }\r\n }\r\n this.adapter.log.info(`\uD83D\uDD11 MQTT Client ${ipMsg} successfully authenticated.`);\r\n callback(null, true);\r\n } catch (e) {\r\n this.adapter.log.error(this.adapter.err2Str(e));\r\n callback(null, false);\r\n }\r\n };\r\n\r\n /**\r\n * fired when a client connects\r\n */\r\n this.aedes.on('client', (client) => {\r\n try {\r\n if (!client) return;\r\n\r\n // Create device entry with id as key, if not yet existing (should have been set in this.aedes.authenticate already)\r\n if (!this.devices[client.id]) this.devices[client.id] = {};\r\n\r\n // IP\r\n const ip = this.devices[client.id].ip;\r\n const ipMsg = ip ? `${this.adapter.fullysEnbl[ip].name} (${ip})` : `${client.id} (IP unknown)`;\r\n\r\n this.adapter.log.debug(`[MQTT] Client ${ipMsg} connected to broker ${this.aedes.id}`);\r\n this.adapter.log.info(`\uD83D\uDD17 MQTT Client ${ipMsg} successfully connected.`);\r\n //this.adapter.log.debug(inspect(client)); //https://stackoverflow.com/a/31557814\r\n\r\n // set isAlive\r\n this.setIsAlive(client.id, true, 'client connected');\r\n\r\n // Schedule check if still alive\r\n this.scheduleCheckIfStillActive(client.id);\r\n } catch (e) {\r\n this.adapter.log.error(this.adapter.err2Str(e));\r\n return;\r\n }\r\n });\r\n\r\n /**\r\n * fired when a client publishes a message packet on the topic\r\n */\r\n this.aedes.on('publish', (packet, client) => {\r\n try {\r\n if (!client || !packet) return;\r\n\r\n this.setIsAlive(client.id, true, 'client published message');\r\n\r\n // Create device entry with id as key, if not yet existing\r\n if (!this.devices[client.id]) this.devices[client.id] = {};\r\n\r\n // QOS is always 1 per Fully documentation\r\n if (packet.qos !== 1) return;\r\n\r\n if (packet.retain) {\r\n /**\r\n * Device Info coming in...\r\n * Per fully documentation: The complete device info will be published every 60 seconds as fully/deviceInfo/[deviceId] topic (retaining, QOS=1).\r\n */\r\n\r\n // Payload as object\r\n const info = JSON.parse(packet.payload.toString());\r\n\r\n // Verification of device info packet\r\n // We don't use topic to check since we do not want to rely on user's input in Fully Browser \"MQTT Device Info Topic\" settings.\r\n if (!('startUrl' in info) && !('ip4' in info)) {\r\n this.adapter.log.error(`[MQTT] Packet rejected: ${info.ip4} - Info packet expected, but ip4 and startUrl is not defined in packet. ${info.deviceId}`);\r\n return;\r\n }\r\n\r\n // IP\r\n const ip = info.ip4;\r\n const devMsg = `${this.adapter.fullysEnbl[ip].name} (${ip})`;\r\n // Check IP - already done in this.aedes.authenticate, but just in case we were unable to get ip there\r\n if (!Object.keys(this.adapter.fullysEnbl).includes(ip)) {\r\n this.adapter.log.error(`[MQTT] Client ${devMsg} Packet rejected: IP is not allowed per adapter settings. ${client.id}`);\r\n return;\r\n }\r\n this.devices[client.id].ip = ip;\r\n\r\n // Slow down: Don't accept info event more often than x seconds\r\n // Per Fully doc, should not come in more often than 60s anyway...\r\n const prevTime = this.devices[client.id].previousInfoPublishTime;\r\n const limit = this.adapter.config.mqttPublishedInfoDelay * 1000; // milliseconds\r\n if (prevTime && prevTime !== 0) {\r\n if (Date.now() - prevTime < limit) {\r\n const diffMs = Date.now() - prevTime;\r\n this.adapter.log.silly(`[MQTT] ${devMsg} Packet rejected: Last packet came in ${diffMs}ms (${Math.round(diffMs / 1000)}s) ago...`);\r\n return;\r\n }\r\n }\r\n this.devices[client.id].previousInfoPublishTime = Date.now(); // set for future events\r\n\r\n /**\r\n * First time received device info incl. IP address etc.\r\n */\r\n if (!this.devices[client.id].mqttFirstReceived) {\r\n // show only once\r\n this.adapter.log.debug(`[MQTT] Client ${client.id} = ${this.adapter.fullysEnbl[ip].name} = ${ip}`);\r\n // set to true\r\n this.devices[client.id].mqttFirstReceived = true;\r\n }\r\n /**\r\n * Call Adapter function onMqttInfo()\r\n */\r\n const result = {\r\n clientId: client.id,\r\n ip: ip,\r\n topic: packet.topic,\r\n infoObj: info,\r\n };\r\n this.adapter.onMqttInfo(result);\r\n } else if (packet.qos === 1 && !packet.retain) {\r\n /**\r\n * Event coming in...\r\n * Per fully documentation: Events will be published as fully/event/[eventId]/[deviceId] topic (non-retaining, QOS=1).\r\n */\r\n // {\"deviceId\":\"xxxxxxxx-xxxxxxxx\",\"event\":\"screenOn\"}\r\n // NOTE: Device ID is different to client id, we actually disregard deviceId\r\n const msg = JSON.parse(packet.payload.toString());\r\n\r\n // Verification of event packet\r\n // We don't use topic to check since we do not want to rely on user's input in Fully Browser \"MQTT Event Topic\" settings.\r\n if (!('event' in msg)) {\r\n this.adapter.log.error(`[MQTT] Packet rejected: Event packet expected, but event is not defined in packet. ${client.id}`);\r\n return;\r\n }\r\n\r\n // Disregard first event once connected: mqttConnected\r\n if (msg.event === 'mqttConnected') {\r\n this.adapter.log.silly(`[MQTT] Client Publish Event: Disregard mqttConnected event - ${msg.deviceId}`);\r\n return;\r\n }\r\n\r\n // Get IP\r\n if (!this.devices[client.id]) {\r\n this.adapter.log.info(`[MQTT] Client Publish Event: Device ID and according IP not yet seen thru \"Publish Info\"`);\r\n this.adapter.log.info(`[MQTT] We wait until first info is published. ${msg.deviceId}`);\r\n return;\r\n }\r\n const ip = this.devices[client.id].ip ? this.devices[client.id].ip : '';\r\n if (ip === '' || typeof ip !== 'string') {\r\n this.adapter.log.debug(`[MQTT] Client Publish Event: IP address could not be determined. - Client ID: ${client.id}`);\r\n this.adapter.log.debug(`[MQTT] Please be patient until first MQTT info packet coming in (takes up to 1 minute)`);\r\n return; // Disregard since IP is unknown!\r\n }\r\n\r\n // Call function\r\n const result = {\r\n clientId: client.id,\r\n ip: ip,\r\n topic: packet.topic,\r\n cmd: msg.event,\r\n };\r\n if (!this.devices[client.id].mqttFirstReceived) {\r\n // show only once\r\n this.adapter.log.info(`[MQTT] \uD83D\uDD17 Client ${client.id} = ${this.adapter.fullysEnbl[ip].name} (${ip})`);\r\n this.devices[client.id].mqttFirstReceived = true;\r\n }\r\n /**\r\n * Call Adapter function onMqttEvent()\r\n */\r\n this.adapter.onMqttEvent(result);\r\n } else {\r\n // Ignore\r\n return;\r\n }\r\n } catch (e) {\r\n this.adapter.log.error(this.adapter.err2Str(e));\r\n return;\r\n }\r\n });\r\n\r\n /**\r\n * fired when a client disconnects\r\n */\r\n this.aedes.on('clientDisconnect', (client) => {\r\n const ip = this.devices[client.id].ip;\r\n const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id;\r\n if (this.adapter.config.mqttConnErrorsAsInfo) {\r\n this.adapter.log.info(`MQTT Client ${logMsgName} disconnected.`);\r\n } else {\r\n this.adapter.log.error(`[MQTT] Client ${logMsgName} disconnected.`);\r\n }\r\n this.setIsAlive(client.id, false, 'client disconnected');\r\n });\r\n\r\n /**\r\n * fired on client error\r\n */\r\n this.aedes.on('clientError', (client, e) => {\r\n if (this.notAuthorizedClients.includes(client.id)) return; // Error msg was already thrown in aedes.authenticate() before\r\n const ip = this.devices[client.id].ip;\r\n const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id;\r\n if (this.adapter.config.mqttConnErrorsAsInfo) {\r\n this.adapter.log.info(`[MQTT] ${logMsgName}: Client error - ${e.message}`);\r\n } else {\r\n this.adapter.log.error(`[MQTT]\uD83D\uDD25 ${logMsgName}: Client error - ${e.message}`);\r\n }\r\n this.adapter.log.debug(`[MQTT]\uD83D\uDD25 ${logMsgName}: Client error - stack: ${e.stack}`);\r\n this.setIsAlive(client.id, false, 'client error');\r\n });\r\n\r\n this.aedes.on('connectionError', (client, e) => {\r\n const ip = this.devices[client.id].ip;\r\n const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id;\r\n if (this.adapter.config.mqttConnErrorsAsInfo) {\r\n this.adapter.log.info(`[MQTT] ${logMsgName}: Connection error - ${e.message}`);\r\n } else {\r\n this.adapter.log.error(`[MQTT]\uD83D\uDD25 ${logMsgName}: Connection error - ${e.message}`);\r\n }\r\n this.adapter.log.debug(`[MQTT]\uD83D\uDD25 ${logMsgName}: Connection error - stack: ${e.stack}`);\r\n this.setIsAlive(client.id, false, 'connection error');\r\n });\r\n\r\n /**\r\n * fired on server error\r\n */\r\n this.server.on('error', (e: any) => {\r\n if (e instanceof Error && e.message.startsWith('listen EADDRINUSE')) {\r\n this.adapter.log.debug(`[MQTT] Cannot start server - ${e.message}`);\r\n this.adapter.log.error(`[MQTT]\uD83D\uDD25 Cannot start server - Port ${this.port} is already in use. Try a different port!`);\r\n } else {\r\n this.adapter.log.error(`[MQTT]\uD83D\uDD25 Cannot start server - ${e.message}`);\r\n }\r\n this.terminate();\r\n });\r\n } catch (e) {\r\n this.adapter.log.error(this.adapter.err2Str(e));\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * If Client is alive or not\r\n */\r\n private setIsAlive(clientId: string, isAlive: true | false, msg: string): void {\r\n if (isAlive) this.devices[clientId].lastTimeActive = Date.now();\r\n this.devices[clientId].isActive = isAlive;\r\n\r\n const ip = this.devices[clientId]?.ip;\r\n if (ip) {\r\n // Call Adapter function onMqttAliveChange()\r\n this.adapter.onMqttAlive(ip, isAlive, msg);\r\n if (isAlive) {\r\n this.scheduleCheckIfStillActive(clientId); // restart timer\r\n } else {\r\n // clear timer\r\n // @ts-expect-error \"Type 'null' is not assignable to type 'Timeout'.ts(2345)\" - we check for not being null via \"if\"\r\n if (this.devices[clientId].timeoutNoUpdate) this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate);\r\n }\r\n } else {\r\n this.adapter.log.debug(`[MQTT] isAlive changed to ${isAlive}, but IP of client ${clientId} is still unknown.`);\r\n }\r\n }\r\n\r\n /**\r\n * Schedule: Check if MQTT topic was sent last x seconds ago\r\n * @param ip IP Address\r\n * @returns void\r\n */\r\n private async scheduleCheckIfStillActive(clientId: string): Promise {\r\n try {\r\n const ip = this.devices[clientId].ip;\r\n const ipMsg = ip ? `${this.adapter.fullysEnbl[ip].name} (${ip})` : `${clientId} (IP unknown)`;\r\n // this.adapter.log.debug(`[MQTT] ${ipMsg}: - Start scheduleCheckIfStillActive`);\r\n\r\n // @ts-expect-error \"Type 'null' is not assignable to type 'Timeout'.ts(2345)\" - we check for not being null via \"if\"\r\n if (this.devices[clientId].timeoutNoUpdate) this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate);\r\n\r\n if (!this.devices[clientId]) this.devices[clientId] = {};\r\n\r\n const interval = 70 * 1000; // every 60s + 10s buffer\r\n this.devices[clientId].timeoutNoUpdate = this.adapter.setTimeout(async () => {\r\n try {\r\n const lastTimeActive = this.devices[clientId].lastTimeActive;\r\n if (!lastTimeActive) return;\r\n const diff = Date.now() - lastTimeActive;\r\n if (diff > 70000) {\r\n this.adapter.log.debug(`[MQTT] ${ipMsg} NOT ALIVE - last contact ${Math.round(diff / 1000)}s (${diff}ms) ago`);\r\n this.setIsAlive(clientId, false, 'client did not send message for more than 70 seconds');\r\n } else {\r\n this.adapter.log.warn(`[MQTT] ${ipMsg} Please open a issue on Github, this should never happen: scheduleCheckIfStillActive() timeout, and last contact was less than 70s ago.`);\r\n this.adapter.log.warn(`[MQTT] ${ipMsg} is alive - last contact ${Math.round(diff / 1000)}s (${diff}ms) ago`);\r\n this.setIsAlive(clientId, true, `alive check is successful (last contact: ${Math.round(diff / 1000)}s ago)`);\r\n }\r\n // Call function again since we are in callback of timeout\r\n this.scheduleCheckIfStillActive(clientId);\r\n } catch (e) {\r\n this.adapter.log.error(this.adapter.err2Str(e));\r\n return;\r\n }\r\n }, interval);\r\n } catch (e) {\r\n this.adapter.log.error(this.adapter.err2Str(e));\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * Terminate MQTT Server and close all...\r\n */\r\n public terminate(): void {\r\n this.adapter.log.info(`[MQTT] Disconnect all clients and close server`);\r\n // isAlive\r\n for (const clientId in this.devices) {\r\n // @ts-expect-error \"Type 'null' is not assignable to type 'Timeout'.ts(2345)\" - we check for not being null via \"if\"\r\n if (this.devices[clientId].timeoutNoUpdate) this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate);\r\n this.setIsAlive(clientId, false, 'MQTT server was terminated');\r\n }\r\n\r\n if (this.aedes) {\r\n this.aedes.close(() => {\r\n this.adapter.log.debug('[MQTT] aedes.close() succeeded');\r\n if (this.server) {\r\n this.server.close(() => {\r\n this.adapter.log.debug('[MQTT] server.close() succeeded');\r\n });\r\n }\r\n });\r\n } else if (this.server) {\r\n this.server.close(() => {\r\n this.adapter.log.debug('[MQTT] server.close() succeeded');\r\n });\r\n }\r\n }\r\n}\r\n"], "mappings": "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", "names": ["Aedes", "net"] } diff --git a/build/lib/restApi.js.map b/build/lib/restApi.js.map index 465b7fd..de45f2e 100644 --- a/build/lib/restApi.js.map +++ b/build/lib/restApi.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../src/lib/restApi.ts"], - "sourcesContent": ["/**\n * REST API Class\n * Purpose: sending commands to Fully, since sending via MQTT is not supported by Fully.\n */\n\nimport axios from 'axios';\nimport { FullyMqtt } from '../main';\nimport { IDevice } from './interfaces';\n\n/**\n * @class RestApi\n * @desc To send commands via REST API to Fully Browser\n */\nexport class RestApiFully {\n /**\n * Constants and Variables\n */\n private readonly adapter: FullyMqtt;\n\n /**\n * Class Constructor\n * @param adapter - ioBroker adapter instance object\n */\n public constructor(adapter: FullyMqtt) {\n this.adapter = adapter;\n }\n\n /**\n * Send a command to Fully\n * @param device - device object\n * @param cmd - 'loadStartURL', 'screenOn', etc.\n * @param val - state value\n * @returns true if successful, false if not\n */\n public async sendCmd(device: IDevice, cmd: string, val: any): Promise {\n try {\n interface ISendCmd {\n urlParameter: string;\n cleanSpaces?: true;\n encode?: true;\n }\n const cmds: { [k: string]: ISendCmd } = {\n textToSpeech: { urlParameter: 'cmd=textToSpeech&text=', cleanSpaces: true, encode: true },\n loadURL: { urlParameter: 'cmd=loadURL&url=', cleanSpaces: true, encode: true },\n startApplication: { urlParameter: 'cmd=startApplication&package=', cleanSpaces: true },\n screenBrightness: { urlParameter: 'cmd=setStringSetting&key=screenBrightness&value=' },\n setAudioVolume: { urlParameter: 'cmd=setAudioVolume&stream=3&level=' },\n };\n let finalUrlParam = '';\n if (cmd in cmds) {\n if (cmds[cmd].cleanSpaces) {\n val = val.toString().trim();\n val = val.replace(/\\s+/g, ' ');\n }\n if (cmds[cmd].encode) {\n val = val.toString().trim();\n val = encodeURIComponent(val);\n }\n finalUrlParam = cmds[cmd].urlParameter + val;\n } else {\n finalUrlParam = 'cmd=' + cmd;\n }\n\n const result = await this.axiosSendCmd(device, cmd, finalUrlParam);\n return result;\n } catch (e) {\n this.adapter.log.error(`[REST] ${device.name}: ${this.adapter.err2Str(e)}`);\n return false;\n }\n }\n\n /**\n * Axios: Send Command\n * @param device - device object\n * @param cmd - Command like \"screenOff\"\n * @param urlParam - URL parameter like \"cmd=screenOff\"\n * @returns false if error, true if successful\n */\n private async axiosSendCmd(device: IDevice, cmd: string, urlParam: string): Promise {\n // Base URL\n const url = `${device.restProtocol}://${device.ip}:${device.restPort}/?password=${this.encodePassword(device.restPassword)}&type=json&${urlParam}`;\n\n // Axios config\n const config = {\n method: 'get',\n timeout: this.adapter.config.restTimeout,\n };\n\n try {\n // Log\n let urlHiddenPassword = url;\n urlHiddenPassword = urlHiddenPassword.replace(/password=.*&type/g, 'password=(hidden)&type');\n this.adapter.log.debug(`[REST] ${device.name}: Start sending command ${cmd}, URL: ${urlHiddenPassword}`);\n\n // Axios: Send command\n const response = await axios.get(url, config);\n\n // Errors\n if (response.status !== 200) {\n this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: ${response.status} - ${response.statusText}`);\n return false;\n }\n if (!('status' in response)) {\n this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but it does not have key 'status'`);\n return false;\n }\n if (!('data' in response)) {\n this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but it does not have key 'data'`);\n return false;\n }\n this.adapter.log.debug(`[REST] ${device.name}: Sending command ${cmd} response.data: ${JSON.stringify(response.data)}`);\n\n if (!('status' in response.data)) {\n this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but response.data does not have key 'status'`);\n return false;\n }\n switch (response.data.status) {\n case 'OK':\n this.adapter.log.debug(`[REST] ${device.name}: Sending command ${cmd} successful: - Status = \"${response.data.status}\", Message = \"${response.data.statustext}\"`);\n return true;\n case 'Error':\n if (response.data.statustext === 'Please login') {\n this.adapter.log.error(`[REST] ${device.name}: Error: Remote Admin Password seems to be incorrect. Sending command ${cmd} failed.`);\n } else {\n this.adapter.log.error(`[REST] ${device.name}: Error: Sending command ${cmd} failed, received status text: ${response.data.statustext}`);\n }\n return false;\n default:\n // Unexpected\n this.adapter.log.error(`[REST] ${device.name}: Undefined response.data.status = \"${response.data.status}\" when sending command ${cmd}: ${response.status} - ${response.statusText}`);\n return false;\n }\n } catch (err) {\n const errTxt = `[REST] ${device.name}: Sending command ${cmd} failed`;\n if (axios.isAxiosError(err)) {\n if (!err?.response) {\n this.adapter.log.warn(`${errTxt}: No response`);\n } else if (err.response?.status === 400) {\n this.adapter.log.error('${errTxt}: Login Failed - Error 400 - ' + err.response?.statusText);\n } else if (err.response?.status) {\n this.adapter.log.error(`${errTxt}: ${err.response.status} - ${err.response.statusText}`);\n } else {\n this.adapter.log.error(`${errTxt}: General Error`);\n }\n } else {\n this.adapter.log.error(`${errTxt}: Error: ${this.adapter.err2Str(err)}`);\n }\n return false;\n }\n }\n\n /**\n * To encode a password to be sent to web server\n * Source: fixedEncodeURIComponent() from https://github.com/arteck/ioBroker.fullybrowser/blob/master/main.js\n * @param pw Password\n * @returns Encoded password\n */\n private encodePassword(pw: string): string {\n return encodeURIComponent(pw).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\n }\n}\n"], + "sourcesContent": ["/**\r\n * REST API Class\r\n * Purpose: sending commands to Fully, since sending via MQTT is not supported by Fully.\r\n */\r\n\r\nimport axios from 'axios';\r\nimport { FullyMqtt } from '../main';\r\nimport { IDevice } from './interfaces';\r\n\r\n/**\r\n * @class RestApi\r\n * @desc To send commands via REST API to Fully Browser\r\n */\r\nexport class RestApiFully {\r\n /**\r\n * Constants and Variables\r\n */\r\n private readonly adapter: FullyMqtt;\r\n\r\n /**\r\n * Class Constructor\r\n * @param adapter - ioBroker adapter instance object\r\n */\r\n public constructor(adapter: FullyMqtt) {\r\n this.adapter = adapter;\r\n }\r\n\r\n /**\r\n * Send a command to Fully\r\n * @param device - device object\r\n * @param cmd - 'loadStartURL', 'screenOn', etc.\r\n * @param val - state value\r\n * @returns true if successful, false if not\r\n */\r\n public async sendCmd(device: IDevice, cmd: string, val: any): Promise {\r\n try {\r\n interface ISendCmd {\r\n urlParameter: string;\r\n cleanSpaces?: true;\r\n encode?: true;\r\n }\r\n const cmds: { [k: string]: ISendCmd } = {\r\n textToSpeech: { urlParameter: 'cmd=textToSpeech&text=', cleanSpaces: true, encode: true },\r\n loadURL: { urlParameter: 'cmd=loadURL&url=', cleanSpaces: true, encode: true },\r\n startApplication: { urlParameter: 'cmd=startApplication&package=', cleanSpaces: true },\r\n screenBrightness: { urlParameter: 'cmd=setStringSetting&key=screenBrightness&value=' },\r\n setAudioVolume: { urlParameter: 'cmd=setAudioVolume&stream=3&level=' },\r\n };\r\n let finalUrlParam = '';\r\n if (cmd in cmds) {\r\n if (cmds[cmd].cleanSpaces) {\r\n val = val.toString().trim();\r\n val = val.replace(/\\s+/g, ' ');\r\n }\r\n if (cmds[cmd].encode) {\r\n val = val.toString().trim();\r\n val = encodeURIComponent(val);\r\n }\r\n finalUrlParam = cmds[cmd].urlParameter + val;\r\n } else {\r\n finalUrlParam = 'cmd=' + cmd;\r\n }\r\n\r\n const result = await this.axiosSendCmd(device, cmd, finalUrlParam);\r\n return result;\r\n } catch (e) {\r\n this.adapter.log.error(`[REST] ${device.name}: ${this.adapter.err2Str(e)}`);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Axios: Send Command\r\n * @param device - device object\r\n * @param cmd - Command like \"screenOff\"\r\n * @param urlParam - URL parameter like \"cmd=screenOff\"\r\n * @returns false if error, true if successful\r\n */\r\n private async axiosSendCmd(device: IDevice, cmd: string, urlParam: string): Promise {\r\n // Base URL\r\n const url = `${device.restProtocol}://${device.ip}:${device.restPort}/?password=${this.encodePassword(device.restPassword)}&type=json&${urlParam}`;\r\n\r\n // Axios config\r\n const config = {\r\n method: 'get',\r\n timeout: this.adapter.config.restTimeout,\r\n };\r\n\r\n try {\r\n // Log\r\n let urlHiddenPassword = url;\r\n urlHiddenPassword = urlHiddenPassword.replace(/password=.*&type/g, 'password=(hidden)&type');\r\n this.adapter.log.debug(`[REST] ${device.name}: Start sending command ${cmd}, URL: ${urlHiddenPassword}`);\r\n\r\n // Axios: Send command\r\n const response = await axios.get(url, config);\r\n\r\n // Errors\r\n if (response.status !== 200) {\r\n this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: ${response.status} - ${response.statusText}`);\r\n return false;\r\n }\r\n if (!('status' in response)) {\r\n this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but it does not have key 'status'`);\r\n return false;\r\n }\r\n if (!('data' in response)) {\r\n this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but it does not have key 'data'`);\r\n return false;\r\n }\r\n this.adapter.log.debug(`[REST] ${device.name}: Sending command ${cmd} response.data: ${JSON.stringify(response.data)}`);\r\n\r\n if (!('status' in response.data)) {\r\n this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but response.data does not have key 'status'`);\r\n return false;\r\n }\r\n switch (response.data.status) {\r\n case 'OK':\r\n this.adapter.log.debug(`[REST] ${device.name}: Sending command ${cmd} successful: - Status = \"${response.data.status}\", Message = \"${response.data.statustext}\"`);\r\n return true;\r\n case 'Error':\r\n if (response.data.statustext === 'Please login') {\r\n this.adapter.log.error(`[REST] ${device.name}: Error: Remote Admin Password seems to be incorrect. Sending command ${cmd} failed.`);\r\n } else {\r\n this.adapter.log.error(`[REST] ${device.name}: Error: Sending command ${cmd} failed, received status text: ${response.data.statustext}`);\r\n }\r\n return false;\r\n default:\r\n // Unexpected\r\n this.adapter.log.error(`[REST] ${device.name}: Undefined response.data.status = \"${response.data.status}\" when sending command ${cmd}: ${response.status} - ${response.statusText}`);\r\n return false;\r\n }\r\n } catch (err) {\r\n const errTxt = `[REST] ${device.name}: Sending command ${cmd} failed`;\r\n if (axios.isAxiosError(err)) {\r\n if (!err?.response) {\r\n this.adapter.log.warn(`${errTxt}: No response`);\r\n } else if (err.response?.status === 400) {\r\n this.adapter.log.error('${errTxt}: Login Failed - Error 400 - ' + err.response?.statusText);\r\n } else if (err.response?.status) {\r\n this.adapter.log.error(`${errTxt}: ${err.response.status} - ${err.response.statusText}`);\r\n } else {\r\n this.adapter.log.error(`${errTxt}: General Error`);\r\n }\r\n } else {\r\n this.adapter.log.error(`${errTxt}: Error: ${this.adapter.err2Str(err)}`);\r\n }\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * To encode a password to be sent to web server\r\n * Source: fixedEncodeURIComponent() from https://github.com/arteck/ioBroker.fullybrowser/blob/master/main.js\r\n * @param pw Password\r\n * @returns Encoded password\r\n */\r\n private encodePassword(pw: string): string {\r\n return encodeURIComponent(pw).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\r\n }\r\n}\r\n"], "mappings": "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", "names": ["axios"] } diff --git a/build/main.js.map b/build/main.js.map index 48c56d7..0961e99 100644 --- a/build/main.js.map +++ b/build/main.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/main.ts"], - "sourcesContent": ["/**\n * -------------------------------------------------------------------\n * ioBroker Fully Browser MQTT Adapter\n * @github https://github.com/Acgua/ioBroker.fully-mqtt\n * @forum https://forum.iobroker.net/topic/63705/\n * @author Acgua \n * @license Apache License 2.0\n * -------------------------------------------------------------------\n */\n\n/**\n * For all imported NPM modules, open console, change dir for example to \"C:\\iobroker\\node_modules\\ioBroker.fully-mqtt\\\"\n * and execute \"npm install \", e.g., npm install axios\n */\nimport * as utils from '@iobroker/adapter-core';\nimport { CONST } from './lib/constants';\nimport { ICmds, IDevice } from './lib/interfaces';\nimport { cleanDeviceName, err2Str, getConfigValuePerKey, isEmpty, isIpAddressValid, wait } from './lib/methods';\nimport { MqttServer } from './lib/mqtt-server';\nimport { RestApiFully } from './lib/restApi';\n\n/**\n * Main ioBroker Adapter Class\n */\nexport class FullyMqtt extends utils.Adapter {\n // Imported methods from ./lib/methods\n public err2Str = err2Str.bind(this);\n public isEmpty = isEmpty.bind(this);\n public wait = wait.bind(this);\n public cleanDeviceName = cleanDeviceName.bind(this);\n public getConfigValuePerKey = getConfigValuePerKey.bind(this);\n public isIpAddressValid = isIpAddressValid.bind(this);\n\n // MQTT Server\n private mqtt_Server: MqttServer | undefined;\n\n // REST API\n private restApi_inst = new RestApiFully(this);\n\n /**\n * Fullys: IP as key, and object per IDevice\n * {\n * '192.168.10.20': {name: 'Tablet Kitchen', id:'Tablet-Kitchen', ip:'192.168.10.20', ...},\n * '192.168.10.30': {name: 'Tablet Hallway', id:'Tablet-Hallway', ip:'192.168.10.30', ...},\n * }\n * Note: we can use this.getFullyPerKey() to get fully object per provided key\n */\n public fullysEnbl: { [ip: string]: IDevice } = {}; // enabled Fullys only\n public fullysDisbl: { [ip: string]: IDevice } = {}; // not enabled Fullys only\n public fullysAll: { [ip: string]: IDevice } = {}; // enabled and not enabled Fullys\n\n // Has onMqttAlive() ever been called before?\n private onMqttAlive_EverBeenCalledBefore = false;\n\n /**\n * Constructor\n */\n public constructor(options: Partial = {}) {\n super({ ...options, name: 'fully-mqtt' });\n this.on('ready', this.onReady.bind(this));\n this.on('stateChange', this.onStateChange.bind(this));\n this.on('unload', this.onUnload.bind(this));\n }\n\n /**\n * Is called when databases are connected and adapter received configuration.\n */\n private async onReady(): Promise {\n try {\n /**\n * Set the connection indicator to false during startup\n */\n this.setState('info.connection', { val: false, ack: true });\n\n /**\n * Verify and init configuration\n */\n if (await this.initConfig()) {\n this.log.debug(`Adapter settings successfully verified and initialized.`);\n } else {\n this.log.error(`Adapter settings initialization failed. ---> Please check your adapter instance settings!`);\n return;\n }\n\n for (const ip in this.fullysEnbl) {\n // Create Fully device objects\n const res = await this.createFullyDeviceObjects(this.fullysEnbl[ip]);\n\n // REST API: Subscribe to command state changes\n if (res) await this.subscribeStatesAsync(this.fullysEnbl[ip].id + '.Commands.*');\n\n // Set enabled and alive states\n this.setState(this.fullysEnbl[ip].id + '.enabled', { val: true, ack: true });\n this.setState(this.fullysEnbl[ip].id + '.alive', { val: false, ack: true });\n }\n // Not enabled fullys (if object exists at all): 1. Enabled state to false; 2. alive to null\n for (const ip in this.fullysDisbl) {\n if (await this.getObjectAsync(this.fullysAll[ip].id)) {\n this.setState(this.fullysDisbl[ip].id + '.enabled', { val: false, ack: true });\n this.setState(this.fullysDisbl[ip].id + '.alive', { val: null, ack: true });\n }\n }\n\n /**\n * Start MQTT Server\n */\n this.mqtt_Server = new MqttServer(this);\n this.mqtt_Server.start();\n\n /**\n * Delete device object tree(s) if deleted or renamed in config\n */\n this.deleteRemovedDeviceObjects();\n } catch (e) {\n this.log.error(this.err2Str(e));\n return;\n }\n }\n\n /**\n * Create Fully Browser Device ioBroker state objects\n * @param device Fully Browser Device Object\n * @returns true if successful, false if error\n */\n private async createFullyDeviceObjects(device: IDevice): Promise {\n try {\n /**\n * Create device object(s)\n */\n // Device and Info object\n await this.setObjectNotExistsAsync(device.id, {\n type: 'device',\n common: {\n name: device.name,\n //@ts-expect-error - Object \"statusStates\" is needed for status, error is: Object literal may only specify known properties, and 'statusStates' does not exist in type 'DeviceCommon'.ts(2345)\n statusStates: { onlineId: `${this.namespace}.${device.id}.alive` },\n },\n native: {},\n });\n await this.setObjectNotExistsAsync(device.id + '.Info', { type: 'channel', common: { name: 'Device Information' }, native: {} });\n\n // Alive\n await this.setObjectNotExistsAsync(device.id + '.alive', {\n type: 'state',\n common: {\n name: 'Is Fully alive?',\n desc: 'If Fully Browser is alive or not',\n type: 'boolean',\n role: 'indicator.reachable',\n icon: 'data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iTXVpU3ZnSWNvbi1yb290IE11aVN2Z0ljb24tZm9udFNpemVNZWRpdW0gaWNvbk93biBjc3MtdnViYnV2IiBmb2N1c2FibGU9ImZhbHNlIiBhcmlhLWhpZGRlbj0idHJ1ZSIgdmlld0JveD0iMCAwIDI0IDI0IiBkYXRhLXRlc3RpZD0iV2lmaUljb24iPjxwYXRoIGQ9Im0xIDkgMiAyYzQuOTctNC45NyAxMy4wMy00Ljk3IDE4IDBsMi0yQzE2LjkzIDIuOTMgNy4wOCAyLjkzIDEgOXptOCA4IDMgMyAzLTNjLTEuNjUtMS42Ni00LjM0LTEuNjYtNiAwem0tNC00IDIgMmMyLjc2LTIuNzYgNy4yNC0yLjc2IDEwIDBsMi0yQzE1LjE0IDkuMTQgOC44NyA5LjE0IDUgMTN6Ij48L3BhdGg+PC9zdmc+',\n read: true,\n write: false,\n },\n native: {},\n });\n // Last info update, and if enabled in adapter settings\n await this.setObjectNotExistsAsync(device.id + '.lastInfoUpdate', { type: 'state', common: { name: 'Last information update', desc: 'Date/time of last information update from Fully Browser', type: 'number', role: 'value.time', read: true, write: false }, native: {} });\n await this.setObjectNotExistsAsync(device.id + '.enabled', { type: 'state', common: { name: 'Is device enabled in adapter settings?', desc: 'If this device is enabled in the adapter settings', type: 'boolean', role: 'indicator', read: true, write: false }, native: {} });\n\n // REST API Commands Objects\n await this.setObjectNotExistsAsync(device.id + '.Commands', { type: 'channel', common: { name: 'Commands' }, native: {} });\n const allCommands = CONST.cmds.concat(CONST.cmdsSwitches); // join both arrays\n for (const cmdObj of allCommands) {\n let lpRole = '';\n if (cmdObj.type === 'boolean') lpRole = 'button';\n if (cmdObj.type === 'string') lpRole = 'text';\n if (cmdObj.type === 'number') lpRole = 'value';\n if (cmdObj.cmdOn && cmdObj.cmdOff) lpRole = 'switch';\n await this.setObjectNotExistsAsync(device.id + '.Commands.' + cmdObj.id, { type: 'state', common: { name: 'Command: ' + cmdObj.name, type: cmdObj.type, role: lpRole, read: true, write: true }, native: {} });\n }\n\n // Create MQTT Events Objects\n // Any not yet created objects are created once a new Event is received.\n await this.setObjectNotExistsAsync(device.id + '.Events', { type: 'channel', common: { name: 'MQTT Events' }, native: {} });\n if (this.config.mqttCreateDefaultEventObjects) {\n for (const event of CONST.mqttEvents) {\n await this.setObjectNotExistsAsync(device.id + '.Events.' + event, { type: 'state', common: { name: 'Event: ' + event, type: 'boolean', role: 'switch', read: true, write: false }, native: {} });\n }\n }\n return true;\n } catch (e) {\n this.log.error(this.err2Str(e));\n return false;\n }\n }\n\n /**\n * Delete device objects if device was (a) renamed or (b) deleted from devices table in adapter settings.\n * However, do not delete if it was just set inactive in table.\n */\n private async deleteRemovedDeviceObjects(): Promise {\n try {\n // Get string array of all adapter objects: ['fully-mqtt.0.info', 'fully-mqtt.0.info.connection', ...];\n const adapterObjectsIds: string[] = Object.keys(await this.getAdapterObjectsAsync());\n\n // Get all existing fully device ids of iobroker adapter objects in array: 'fully-mqtt.0.Tablet-Kitchen' -> 'Tablet-Kitchen', 'fully-mqtt.0.Tablet-Hallway' -> 'Tablet-Hallway', etc.\n const allObjectDeviceIds: Array = [];\n for (const objectId of adapterObjectsIds) {\n const deviceId = objectId.split('.')[2]; // e.g. 'Tablet-Kitchen'\n // Ignore fully-mqtt.0.info tree (which includes fully-mqtt.0.info.connection, ...). Add more to ignore as needed in the future...\n if (['info'].includes(deviceId)) {\n this.log.silly(`Cleanup: Ignore non device related state ${objectId}.`);\n } else {\n if (!allObjectDeviceIds.includes(deviceId)) allObjectDeviceIds.push(deviceId);\n }\n }\n\n // Get all adapter configuration device ids (enabled and disabled), like ['Tablet-Kitchen', 'Tablet-Hallway', ...]\n const allConfigDeviceIds: string[] = [];\n for (const ip in this.fullysAll) {\n allConfigDeviceIds.push(this.fullysAll[ip].id);\n }\n // Delete\n for (const id of allObjectDeviceIds) {\n if (!allConfigDeviceIds.includes(id)) {\n await this.delObjectAsync(id, { recursive: true });\n this.log.info(`Cleanup: Deleted no longer defined device objects of '${id}'.`);\n }\n }\n } catch (e) {\n this.log.error(this.err2Str(e));\n return;\n }\n }\n\n /**\n * Verify adapter instance settings\n */\n private async initConfig(): Promise {\n try {\n /*************************\n * MQTT Fields\n *************************/\n if (this.isEmpty(this.config.mqttPort) || this.config.mqttPort < 1 || this.config.mqttPort > 65535) {\n this.log.warn(`Adapter instance settings: MQTT Port ${this.config.mqttPort} is not allowed, set to default of 1886`);\n this.config.mqttPort = 1886;\n }\n if (this.isEmpty(this.config.mqttPublishedInfoDelay) || this.config.mqttPublishedInfoDelay < 2 || this.config.mqttPublishedInfoDelay > 120) {\n this.log.warn(`Adapter instance settings: MQTT Publish Info Delay of ${this.config.mqttPublishedInfoDelay}s is not allowed, set to default of 30s`);\n this.config.mqttPublishedInfoDelay = 30;\n }\n\n /*************************\n * REST API Fields\n *************************/\n if (this.isEmpty(this.config.restTimeout) || this.config.restTimeout < 500 || this.config.restTimeout > 15000) {\n this.log.warn(`Adapter instance settings: REST API timeout of ${this.config.restTimeout} ms is not allowed, set to default of 6000ms`);\n this.config.restTimeout = 6000;\n }\n\n /*************************\n * Table Devices\n *************************/\n if (this.isEmpty(this.config.tableDevices)) {\n this.log.error(`No Fully devices defined in adapter instance settings!`);\n return false;\n }\n const deviceIds: string[] = []; // to check for duplicate device ids\n const deviceIPs: string[] = []; // to check for duplicate device IPs\n for (let i = 0; i < this.config.tableDevices.length; i++) {\n const lpDevice = this.config.tableDevices[i];\n const finalDevice: IDevice = {\n name: '',\n id: '',\n ip: '',\n enabled: false,\n mqttInfoObjectsCreated: false,\n mqttInfoKeys: [],\n restProtocol: 'http',\n restPort: 0,\n restPassword: '',\n lastSeen: 0, // timestamp\n isAlive: false,\n };\n\n // name\n if (this.isEmpty(lpDevice.name)) {\n this.log.error(`Provided device name \"${lpDevice.name}\" is empty!`);\n return false;\n }\n finalDevice.name = lpDevice.name.trim();\n\n // id\n finalDevice.id = this.cleanDeviceName(lpDevice.name);\n if (finalDevice.id.length < 1) {\n this.log.error(`Provided device name \"${lpDevice.name}\" is too short and/or has invalid characters!`);\n return false;\n }\n if (deviceIds.includes(finalDevice.id)) {\n this.log.error(`Device \"${finalDevice.name}\" -> id:\"${finalDevice.id}\" is used for more than once device.`);\n return false;\n } else {\n deviceIds.push(finalDevice.id);\n }\n\n // REST Protocol (http/https)\n if (lpDevice.restProtocol !== 'http' && lpDevice.restProtocol !== 'https') {\n this.log.warn(`${finalDevice.name}: REST API Protocol is empty, set to http as default.`);\n finalDevice.restProtocol = 'http';\n } else {\n finalDevice.restProtocol = lpDevice.restProtocol;\n }\n\n // IP Address\n if (!this.isIpAddressValid(lpDevice.ip)) {\n this.log.error(`${finalDevice.name}: Provided IP address \"${lpDevice.ip}\" is not valid!`);\n return false;\n }\n if (deviceIPs.includes(lpDevice.ip)) {\n this.log.error(`Device \"${finalDevice.name}\" -> IP:\"${lpDevice.ip}\" is used for more than once device.`);\n return false;\n } else {\n deviceIPs.push(lpDevice.ip);\n finalDevice.ip = lpDevice.ip;\n }\n\n // REST Port\n if (isNaN(lpDevice.restPort) || lpDevice.restPort < 0 || lpDevice.restPort > 65535) {\n this.log.error(`Adapter config Fully port number ${lpDevice.restPort} is not valid, should be >= 0 and < 65536.`);\n return false;\n } else {\n finalDevice.restPort = Math.round(lpDevice.restPort);\n }\n // REST Password\n if (isEmpty(lpDevice.restPassword)) {\n this.log.error(`Remote Admin (REST API) Password must not be empty!`);\n return false;\n } else {\n finalDevice.restPassword = lpDevice.restPassword;\n }\n\n // Enabled status\n finalDevice.enabled = lpDevice.enabled ? true : false;\n\n // Debug log of config\n const logConfig = { ...finalDevice }; // copy object using spread\n logConfig.restPassword = '(hidden)'; // do not show password in log !\n this.log.debug(`Final Config: ${JSON.stringify(logConfig)}`);\n\n // Finalize\n this.fullysAll[finalDevice.ip] = finalDevice;\n if (lpDevice.enabled) {\n this.fullysEnbl[finalDevice.ip] = finalDevice;\n this.log.info(`\uD83D\uDDF8 ${finalDevice.name} (${finalDevice.ip}): Config successfully verified.`);\n } else {\n this.fullysDisbl[finalDevice.ip] = finalDevice;\n this.log.info(`${finalDevice.name} (${finalDevice.ip}) is not enabled in settings, so it will not be used by adapter.`);\n }\n }\n\n if (Object.keys(this.fullysEnbl).length === 0) {\n this.log.error(`No active devices with correct configuration found.`);\n return false;\n }\n return true;\n } catch (e) {\n this.log.error(this.err2Str(e));\n return false;\n }\n }\n\n /**\n * On Alive Changes\n * MQTT is being used only, REST API not.\n */\n public async onMqttAlive(ip: string, isAlive: true | false, msg: string): Promise {\n try {\n const prevIsAlive = this.fullysEnbl[ip].isAlive;\n this.fullysEnbl[ip].isAlive = isAlive;\n\n // Has this function ever been called before? If adapter is restarted, we ensure log, etc.\n const calledBefore = this.onMqttAlive_EverBeenCalledBefore; // Keep old value\n this.onMqttAlive_EverBeenCalledBefore = true; // Now it was called\n\n /***********\n * 1 - Fully Device\n ***********/\n // if alive status changed\n if ((!calledBefore && isAlive === true) || prevIsAlive !== isAlive) {\n // Set Device isAlive Status - we could also use setStateChanged()...\n this.setState(this.fullysEnbl[ip].id + '.alive', { val: isAlive, ack: true });\n\n // log\n if (isAlive) {\n this.log.info(`${this.fullysEnbl[ip].name} is alive (MQTT: ${msg})`);\n } else {\n this.log.warn(`${this.fullysEnbl[ip].name} is not alive! (MQTT: ${msg})`);\n }\n } else {\n // No change\n }\n\n /***********\n * 2 - Adapter Connection indicator\n ***********/\n let countAll = 0;\n let countAlive = 0;\n for (const lpIpAddr in this.fullysEnbl) {\n countAll++;\n if (this.fullysEnbl[lpIpAddr].isAlive) {\n countAlive++;\n }\n }\n let areAllAlive = false;\n if (countAll > 0 && countAll === countAlive) areAllAlive = true;\n this.setStateChanged('info.connection', { val: areAllAlive, ack: true });\n } catch (e) {\n this.log.error(this.err2Str(e));\n return;\n }\n }\n\n /**\n * MQTT: once new device info packet is coming in\n */\n public async onMqttInfo(obj: { clientId: string; ip: string; topic: string; infoObj: { [k: string]: any } }): Promise {\n try {\n // log\n this.log.debug(`[MQTT] ${this.fullysEnbl[obj.ip].name} published info, topic: ${obj.topic}`);\n //this.log.debug(`[MQTT] Client ${obj.ip} Publish Info: Details: ${JSON.stringify(obj.infoObj)}`);\n\n // Create info objects if not yet existing\n const formerInfoKeysLength: number = this.fullysEnbl[obj.ip].mqttInfoKeys.length;\n const newInfoKeysAdded: string[] = [];\n for (const key in obj.infoObj) {\n const val = obj.infoObj[key];\n const valType = typeof val;\n // only accept certain types\n if (valType !== 'string' && valType !== 'boolean' && valType !== 'object' && valType !== 'number') {\n this.log.warn(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Unknown type ${valType} of key '${key}' in info object`);\n continue;\n }\n // Create info object if not yet seen - this check is used for increasing performance by not unnesserily call setObjectNotExistsAsync() every time new info package comes in\n if (!this.fullysEnbl[obj.ip].mqttInfoKeys.includes(key)) {\n this.fullysEnbl[obj.ip].mqttInfoKeys.push(key);\n newInfoKeysAdded.push(key);\n await this.setObjectNotExistsAsync(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { type: 'state', common: { name: 'Info: ' + key, type: valType, role: 'value', read: true, write: false }, native: {} });\n }\n }\n if (formerInfoKeysLength === 0) this.log.debug(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Initially create states for ${newInfoKeysAdded.length} info items (if not yet existing)`);\n if (formerInfoKeysLength > 0 && newInfoKeysAdded.length > 0) this.log.info(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Created new info object(s) as not seen before (if object(s) did not exist): ${newInfoKeysAdded.join(', ')}`);\n\n // Set info objects\n for (const key in obj.infoObj) {\n const newVal = typeof obj.infoObj[key] === 'object' ? JSON.stringify(obj.infoObj[key]) : obj.infoObj[key]; // https://forum.iobroker.net/post/628870 - https://forum.iobroker.net/post/960260\n if (this.config.mqttUpdateUnchangedObjects) {\n this.setState(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { val: newVal, ack: true });\n } else {\n this.setStateChanged(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { val: newVal, ack: true });\n }\n }\n this.setState(this.fullysEnbl[obj.ip].id + '.lastInfoUpdate', { val: Date.now(), ack: true });\n this.setState(this.fullysEnbl[obj.ip].id + '.alive', { val: true, ack: true });\n } catch (e) {\n this.log.error(this.err2Str(e));\n return;\n }\n }\n\n /**\n * MQTT: once new event packet is coming in\n */\n public async onMqttEvent(obj: { clientId: string; ip: string; topic: string; cmd: string }): Promise {\n try {\n // log\n this.log.debug(`[MQTT] \uD83D\uDCE1 ${this.fullysEnbl[obj.ip].name} published event, topic: ${obj.topic}, cmd: ${obj.cmd}`);\n\n /**\n * Set Event State\n */\n const pthEvent = `${this.fullysEnbl[obj.ip].id}.Events.${obj.cmd}`;\n if (!(await this.getObjectAsync(pthEvent))) {\n this.log.debug(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Event ${obj.cmd} received but state ${pthEvent} does not exist, so we create it first`);\n await this.setObjectNotExistsAsync(pthEvent, { type: 'state', common: { name: 'Event: ' + obj.cmd, type: 'boolean', role: 'switch', read: true, write: false }, native: {} });\n }\n this.setState(pthEvent, { val: true, ack: true });\n\n /**\n * Confirm Command state(s) with ack: true\n */\n const pthCmd = this.fullysEnbl[obj.ip].id + '.Commands';\n\n // Check if it is a switch with MQTT commands connected\n const idx = this.getIndexFromConf(CONST.cmdsSwitches, ['mqttOn', 'mqttOff'], obj.cmd);\n if (idx !== -1) {\n // We have a switch\n const conf = CONST.cmdsSwitches[idx]; // the found line from config array\n const onOrOffCmd = obj.cmd === conf.mqttOn ? true : false;\n await this.setStateAsync(`${pthCmd}.${conf.id}`, { val: onOrOffCmd, ack: true });\n await this.setStateAsync(`${pthCmd}.${conf.cmdOn}`, { val: onOrOffCmd, ack: true });\n await this.setStateAsync(`${pthCmd}.${conf.cmdOff}`, { val: !onOrOffCmd, ack: true });\n } else {\n // No switch\n const idx = this.getIndexFromConf(CONST.cmds, ['id'], obj.cmd);\n if (idx !== -1 && CONST.cmds[idx].type === 'boolean') {\n // We have a button, so set it to true\n await this.setStateAsync(`${pthCmd}.${obj.cmd}`, { val: true, ack: true });\n } else {\n this.log.silly(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Event cmd ${obj.cmd} - no REST API command is existing, so skip confirmation with with ack:true`);\n }\n }\n } catch (e) {\n this.log.error(this.err2Str(e));\n return;\n }\n }\n\n /**\n * Called once a subscribed state changes.\n * Ready once subscribeStatesAsync() is called...\n * @param id - e.g. \"fully-mqtt.0.Tablet-Bathroom.Commands.screenSwitch\"\n * @param stateObj - e.g. { val: true, ack: false, ts: 123456789, q: 0, lc: 123456789 }\n */\n private async onStateChange(stateId: string, stateObj: ioBroker.State | null | undefined): Promise {\n try {\n if (!stateObj) return; // state was deleted, we disregard...\n if (stateObj.ack) return; // ignore ack:true\n const idSplit = stateId.split('.');\n const deviceId = idSplit[2]; // \"Tablet-Bathroom\"\n const channel = idSplit[3]; // \"Commands\"\n const cmd = idSplit[4]; // \"screenSwitch\"\n const pth = deviceId + '.' + channel; // Tablet-Bathroom.Commands\n /**\n * Commands\n */\n if (channel === 'Commands') {\n this.log.debug(`state ${stateId} changed: ${stateObj.val} (ack = ${stateObj.ack})`);\n // Get device object\n const fully = this.getFullyByKey('id', deviceId);\n if (!fully) throw `Fully object for deviceId '${deviceId}' not found!`;\n\n let cmdToSend: string | undefined = cmd; // Command to send to Fully\n let switchConf: undefined | ICmds = undefined; // Config line of switch\n\n /****************\n * Check if it is a switch state cmd, like 'screenSwitch'\n ****************/\n const idxSw = this.getIndexFromConf(CONST.cmdsSwitches, ['id'], cmd);\n if (idxSw !== -1) {\n // It is a switch\n switchConf = CONST.cmdsSwitches[idxSw]; // the found line from config array\n cmdToSend = stateObj.val ? switchConf.cmdOn : switchConf.cmdOff;\n } else {\n // Not a switch.\n // If val is false, we disregard, since it is a button only\n if (!stateObj.val) return;\n }\n if (!cmdToSend) throw `onStateChange() - ${stateId}: fullyCmd could not be determined!`;\n\n /**\n * Send Command\n */\n const sendCommand = await this.restApi_inst.sendCmd(fully, cmdToSend, stateObj.val);\n if (sendCommand) {\n if (this.config.restCommandLogAsDebug) {\n this.log.debug(`\uD83D\uDDF8 ${fully.name}: Command ${cmd} successfully set to ${stateObj.val}`);\n } else {\n this.log.info(`\uD83D\uDDF8 ${fully.name}: Command ${cmd} successfully set to ${stateObj.val}`);\n }\n /**\n * Confirm with ack:true\n */\n if (switchConf !== undefined) {\n // it is a switch\n const onOrOffCmdVal = cmd === switchConf.cmdOn ? true : false;\n await this.setStateAsync(`${pth}.${switchConf.id}`, { val: onOrOffCmdVal, ack: true });\n await this.setStateAsync(`${pth}.${switchConf.cmdOn}`, { val: onOrOffCmdVal, ack: true });\n await this.setStateAsync(`${pth}.${switchConf.cmdOff}`, { val: !onOrOffCmdVal, ack: true });\n } else {\n // No switch\n if (typeof stateObj.val === 'boolean') {\n const idx = this.getIndexFromConf(CONST.cmds, ['id'], cmd);\n if (idx !== -1) {\n if (CONST.cmds[idx].type === 'boolean') {\n // Is a button\n await this.setStateAsync(stateId, { val: true, ack: true });\n } else {\n // This should actually not happen, as we just define buttons in commands, but anyway\n this.log.warn(`${fully.name}: ${stateId} - val: ${stateObj.val} is boolean, but cmd ${cmd} is not defined in CONF`);\n await this.setStateAsync(stateId, { val: stateObj.val, ack: true });\n }\n } else {\n this.log.warn(`${fully.name}: ${stateId} - val: ${stateObj.val}, cmd ${cmd} is not defined in CONF`);\n }\n } else {\n // Non-boolean, so just set val with ack:true...\n await this.setStateAsync(stateId, { val: stateObj.val, ack: true });\n }\n }\n } else {\n // log, more log lines were already published by this.restApi_inst.sendCmd()\n this.log.debug(`${fully.name}: restApiSendCmd() was not successful (${stateId})`);\n }\n }\n } catch (e) {\n this.log.error(this.err2Str(e));\n return;\n }\n }\n\n /**\n * Get Fully Object per provided key and value\n * {\n * '192.168.10.20': {name: 'Tablet Kitchen', id:'Tablet-Kitchen', ip:'192.168.10.20', ...},\n * '192.168.10.30': {name: 'Tablet Hallway', id:'Tablet-Hallway', ip:'192.168.10.30', ...},\n * }\n * getFullyByKey('id', 'Tablet-Hallway') will return the second object...\n * @param keyId - e.g. 'id', 'name', ...\n * @param value - e.g. 'Tablet Hallway', ...\n * @returns - fully object or false if not found\n */\n private getFullyByKey(keyId: string, value: any): IDevice | false {\n for (const ip in this.fullysEnbl) {\n if (keyId in this.fullysEnbl[ip]) {\n const lpKeyId = keyId as string;\n // Wow, what a line. Due to: https://bobbyhadz.com/blog/typescript-element-implicitly-has-any-type-expression\n const lpVal = this.fullysEnbl[ip][lpKeyId as keyof (typeof this.fullysEnbl)[typeof ip]];\n if (lpVal === value) {\n return this.fullysEnbl[ip];\n }\n }\n }\n return false;\n }\n\n /**\n * Gets Index for given keys and a value\n * @param config - config like CONST.cmds\n * @param keys - like ['mqttOn','mqttOff']\n * @param cmd - like 'onScreensaverStart'\n * @returns Index (0-...), or -1 if not found\n */\n private getIndexFromConf(config: { [k: string]: any }[], keys: string[], cmd: string): number {\n try {\n let index = -1;\n for (const key of keys) {\n // Get array index\n index = config.findIndex((x: { [k: string]: any }) => x[key] === cmd);\n if (index !== -1) break;\n }\n return index;\n } catch (e) {\n this.log.error(this.err2Str(e));\n return -1;\n }\n }\n\n /**\n * Is called when adapter shuts down - callback has to be called under any circumstances!\n */\n private async onUnload(callback: () => void): Promise {\n try {\n // All Fullys: Set alive status to null\n if (this.fullysAll) {\n for (const ip in this.fullysAll) {\n // We check first if object exists, as there were errors in log on when updating adpater via Github (related to missing objects)\n if (await this.getObjectAsync(this.fullysAll[ip].id)) {\n this.setState(this.fullysAll[ip].id + '.alive', { val: null, ack: true });\n }\n }\n }\n\n // Clear MQTT server timeouts\n if (this.mqtt_Server) {\n for (const clientId in this.mqtt_Server.devices) {\n // @ts-expect-error \"Type 'null' is not assignable to type 'Timeout'.ts(2345)\" - we check for not being null via \"if\"\n if (this.mqtt_Server.devices[clientId].timeoutNoUpdate) this.clearTimeout(this.mqtt_Server.devices[clientId].timeoutNoUpdate);\n }\n }\n\n // destroy MQTT Server\n if (this.mqtt_Server) {\n this.mqtt_Server.terminate();\n }\n\n callback();\n } catch (e) {\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial | undefined) => new FullyMqtt(options);\n} else {\n // otherwise start the instance directly\n (() => new FullyMqtt())();\n}\n"], + "sourcesContent": ["/**\r\n * -------------------------------------------------------------------\r\n * ioBroker Fully Browser MQTT Adapter\r\n * @github https://github.com/Acgua/ioBroker.fully-mqtt\r\n * @forum https://forum.iobroker.net/topic/63705/\r\n * @author Acgua \r\n * @license Apache License 2.0\r\n * -------------------------------------------------------------------\r\n */\r\n\r\n/**\r\n * For all imported NPM modules, open console, change dir for example to \"C:\\iobroker\\node_modules\\ioBroker.fully-mqtt\\\"\r\n * and execute \"npm install \", e.g., npm install axios\r\n */\r\nimport * as utils from '@iobroker/adapter-core';\r\nimport { CONST } from './lib/constants';\r\nimport { ICmds, IDevice } from './lib/interfaces';\r\nimport { cleanDeviceName, err2Str, getConfigValuePerKey, isEmpty, isIpAddressValid, wait } from './lib/methods';\r\nimport { MqttServer } from './lib/mqtt-server';\r\nimport { RestApiFully } from './lib/restApi';\r\n\r\n/**\r\n * Main ioBroker Adapter Class\r\n */\r\nexport class FullyMqtt extends utils.Adapter {\r\n // Imported methods from ./lib/methods\r\n public err2Str = err2Str.bind(this);\r\n public isEmpty = isEmpty.bind(this);\r\n public wait = wait.bind(this);\r\n public cleanDeviceName = cleanDeviceName.bind(this);\r\n public getConfigValuePerKey = getConfigValuePerKey.bind(this);\r\n public isIpAddressValid = isIpAddressValid.bind(this);\r\n\r\n // MQTT Server\r\n private mqtt_Server: MqttServer | undefined;\r\n\r\n // REST API\r\n private restApi_inst = new RestApiFully(this);\r\n\r\n /**\r\n * Fullys: IP as key, and object per IDevice\r\n * {\r\n * '192.168.10.20': {name: 'Tablet Kitchen', id:'Tablet-Kitchen', ip:'192.168.10.20', ...},\r\n * '192.168.10.30': {name: 'Tablet Hallway', id:'Tablet-Hallway', ip:'192.168.10.30', ...},\r\n * }\r\n * Note: we can use this.getFullyPerKey() to get fully object per provided key\r\n */\r\n public fullysEnbl: { [ip: string]: IDevice } = {}; // enabled Fullys only\r\n public fullysDisbl: { [ip: string]: IDevice } = {}; // not enabled Fullys only\r\n public fullysAll: { [ip: string]: IDevice } = {}; // enabled and not enabled Fullys\r\n\r\n // Has onMqttAlive() ever been called before?\r\n private onMqttAlive_EverBeenCalledBefore = false;\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(options: Partial = {}) {\r\n super({ ...options, name: 'fully-mqtt' });\r\n this.on('ready', this.onReady.bind(this));\r\n this.on('stateChange', this.onStateChange.bind(this));\r\n this.on('unload', this.onUnload.bind(this));\r\n }\r\n\r\n /**\r\n * Is called when databases are connected and adapter received configuration.\r\n */\r\n private async onReady(): Promise {\r\n try {\r\n /**\r\n * Set the connection indicator to false during startup\r\n */\r\n this.setState('info.connection', { val: false, ack: true });\r\n\r\n /**\r\n * Verify and init configuration\r\n */\r\n if (await this.initConfig()) {\r\n this.log.debug(`Adapter settings successfully verified and initialized.`);\r\n } else {\r\n this.log.error(`Adapter settings initialization failed. ---> Please check your adapter instance settings!`);\r\n return;\r\n }\r\n\r\n for (const ip in this.fullysEnbl) {\r\n // Create Fully device objects\r\n const res = await this.createFullyDeviceObjects(this.fullysEnbl[ip]);\r\n\r\n // REST API: Subscribe to command state changes\r\n if (res) await this.subscribeStatesAsync(this.fullysEnbl[ip].id + '.Commands.*');\r\n\r\n // Set enabled and alive states\r\n this.setState(this.fullysEnbl[ip].id + '.enabled', { val: true, ack: true });\r\n this.setState(this.fullysEnbl[ip].id + '.alive', { val: false, ack: true });\r\n }\r\n // Not enabled fullys (if object exists at all): 1. Enabled state to false; 2. alive to null\r\n for (const ip in this.fullysDisbl) {\r\n if (await this.getObjectAsync(this.fullysAll[ip].id)) {\r\n this.setState(this.fullysDisbl[ip].id + '.enabled', { val: false, ack: true });\r\n this.setState(this.fullysDisbl[ip].id + '.alive', { val: null, ack: true });\r\n }\r\n }\r\n\r\n /**\r\n * Start MQTT Server\r\n */\r\n this.mqtt_Server = new MqttServer(this);\r\n this.mqtt_Server.start();\r\n\r\n /**\r\n * Delete device object tree(s) if deleted or renamed in config\r\n */\r\n this.deleteRemovedDeviceObjects();\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * Create Fully Browser Device ioBroker state objects\r\n * @param device Fully Browser Device Object\r\n * @returns true if successful, false if error\r\n */\r\n private async createFullyDeviceObjects(device: IDevice): Promise {\r\n try {\r\n /**\r\n * Create device object(s)\r\n */\r\n // Device and Info object\r\n await this.setObjectNotExistsAsync(device.id, {\r\n type: 'device',\r\n common: {\r\n name: device.name,\r\n //@ts-expect-error - Object \"statusStates\" is needed for status, error is: Object literal may only specify known properties, and 'statusStates' does not exist in type 'DeviceCommon'.ts(2345)\r\n statusStates: { onlineId: `${this.namespace}.${device.id}.alive` },\r\n },\r\n native: {},\r\n });\r\n await this.setObjectNotExistsAsync(device.id + '.Info', { type: 'channel', common: { name: 'Device Information' }, native: {} });\r\n\r\n // Alive\r\n await this.setObjectNotExistsAsync(device.id + '.alive', {\r\n type: 'state',\r\n common: {\r\n name: 'Is Fully alive?',\r\n desc: 'If Fully Browser is alive or not',\r\n type: 'boolean',\r\n role: 'indicator.reachable',\r\n icon: 'data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iTXVpU3ZnSWNvbi1yb290IE11aVN2Z0ljb24tZm9udFNpemVNZWRpdW0gaWNvbk93biBjc3MtdnViYnV2IiBmb2N1c2FibGU9ImZhbHNlIiBhcmlhLWhpZGRlbj0idHJ1ZSIgdmlld0JveD0iMCAwIDI0IDI0IiBkYXRhLXRlc3RpZD0iV2lmaUljb24iPjxwYXRoIGQ9Im0xIDkgMiAyYzQuOTctNC45NyAxMy4wMy00Ljk3IDE4IDBsMi0yQzE2LjkzIDIuOTMgNy4wOCAyLjkzIDEgOXptOCA4IDMgMyAzLTNjLTEuNjUtMS42Ni00LjM0LTEuNjYtNiAwem0tNC00IDIgMmMyLjc2LTIuNzYgNy4yNC0yLjc2IDEwIDBsMi0yQzE1LjE0IDkuMTQgOC44NyA5LjE0IDUgMTN6Ij48L3BhdGg+PC9zdmc+',\r\n read: true,\r\n write: false,\r\n },\r\n native: {},\r\n });\r\n // Last info update, and if enabled in adapter settings\r\n await this.setObjectNotExistsAsync(device.id + '.lastInfoUpdate', { type: 'state', common: { name: 'Last information update', desc: 'Date/time of last information update from Fully Browser', type: 'number', role: 'value.time', read: true, write: false }, native: {} });\r\n await this.setObjectNotExistsAsync(device.id + '.enabled', { type: 'state', common: { name: 'Is device enabled in adapter settings?', desc: 'If this device is enabled in the adapter settings', type: 'boolean', role: 'indicator', read: true, write: false }, native: {} });\r\n\r\n // REST API Commands Objects\r\n await this.setObjectNotExistsAsync(device.id + '.Commands', { type: 'channel', common: { name: 'Commands' }, native: {} });\r\n const allCommands = CONST.cmds.concat(CONST.cmdsSwitches); // join both arrays\r\n for (const cmdObj of allCommands) {\r\n let lpRole = '';\r\n if (cmdObj.type === 'boolean') lpRole = 'button';\r\n if (cmdObj.type === 'string') lpRole = 'text';\r\n if (cmdObj.type === 'number') lpRole = 'value';\r\n if (cmdObj.cmdOn && cmdObj.cmdOff) lpRole = 'switch';\r\n await this.setObjectNotExistsAsync(device.id + '.Commands.' + cmdObj.id, { type: 'state', common: { name: 'Command: ' + cmdObj.name, type: cmdObj.type, role: lpRole, read: true, write: true }, native: {} });\r\n }\r\n\r\n // Create MQTT Events Objects\r\n // Any not yet created objects are created once a new Event is received.\r\n await this.setObjectNotExistsAsync(device.id + '.Events', { type: 'channel', common: { name: 'MQTT Events' }, native: {} });\r\n if (this.config.mqttCreateDefaultEventObjects) {\r\n for (const event of CONST.mqttEvents) {\r\n await this.setObjectNotExistsAsync(device.id + '.Events.' + event, { type: 'state', common: { name: 'Event: ' + event, type: 'boolean', role: 'switch', read: true, write: false }, native: {} });\r\n }\r\n }\r\n return true;\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Delete device objects if device was (a) renamed or (b) deleted from devices table in adapter settings.\r\n * However, do not delete if it was just set inactive in table.\r\n */\r\n private async deleteRemovedDeviceObjects(): Promise {\r\n try {\r\n // Get string array of all adapter objects: ['fully-mqtt.0.info', 'fully-mqtt.0.info.connection', ...];\r\n const adapterObjectsIds: string[] = Object.keys(await this.getAdapterObjectsAsync());\r\n\r\n // Get all existing fully device ids of iobroker adapter objects in array: 'fully-mqtt.0.Tablet-Kitchen' -> 'Tablet-Kitchen', 'fully-mqtt.0.Tablet-Hallway' -> 'Tablet-Hallway', etc.\r\n const allObjectDeviceIds: Array = [];\r\n for (const objectId of adapterObjectsIds) {\r\n const deviceId = objectId.split('.')[2]; // e.g. 'Tablet-Kitchen'\r\n // Ignore fully-mqtt.0.info tree (which includes fully-mqtt.0.info.connection, ...). Add more to ignore as needed in the future...\r\n if (['info'].includes(deviceId)) {\r\n this.log.silly(`Cleanup: Ignore non device related state ${objectId}.`);\r\n } else {\r\n if (!allObjectDeviceIds.includes(deviceId)) allObjectDeviceIds.push(deviceId);\r\n }\r\n }\r\n\r\n // Get all adapter configuration device ids (enabled and disabled), like ['Tablet-Kitchen', 'Tablet-Hallway', ...]\r\n const allConfigDeviceIds: string[] = [];\r\n for (const ip in this.fullysAll) {\r\n allConfigDeviceIds.push(this.fullysAll[ip].id);\r\n }\r\n // Delete\r\n for (const id of allObjectDeviceIds) {\r\n if (!allConfigDeviceIds.includes(id)) {\r\n await this.delObjectAsync(id, { recursive: true });\r\n this.log.info(`Cleanup: Deleted no longer defined device objects of '${id}'.`);\r\n }\r\n }\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * Verify adapter instance settings\r\n */\r\n private async initConfig(): Promise {\r\n try {\r\n /*************************\r\n * MQTT Fields\r\n *************************/\r\n if (this.isEmpty(this.config.mqttPort) || this.config.mqttPort < 1 || this.config.mqttPort > 65535) {\r\n this.log.warn(`Adapter instance settings: MQTT Port ${this.config.mqttPort} is not allowed, set to default of 1886`);\r\n this.config.mqttPort = 1886;\r\n }\r\n if (this.isEmpty(this.config.mqttPublishedInfoDelay) || this.config.mqttPublishedInfoDelay < 2 || this.config.mqttPublishedInfoDelay > 120) {\r\n this.log.warn(`Adapter instance settings: MQTT Publish Info Delay of ${this.config.mqttPublishedInfoDelay}s is not allowed, set to default of 30s`);\r\n this.config.mqttPublishedInfoDelay = 30;\r\n }\r\n\r\n /*************************\r\n * REST API Fields\r\n *************************/\r\n if (this.isEmpty(this.config.restTimeout) || this.config.restTimeout < 500 || this.config.restTimeout > 15000) {\r\n this.log.warn(`Adapter instance settings: REST API timeout of ${this.config.restTimeout} ms is not allowed, set to default of 6000ms`);\r\n this.config.restTimeout = 6000;\r\n }\r\n\r\n /*************************\r\n * Table Devices\r\n *************************/\r\n if (this.isEmpty(this.config.tableDevices)) {\r\n this.log.error(`No Fully devices defined in adapter instance settings!`);\r\n return false;\r\n }\r\n const deviceIds: string[] = []; // to check for duplicate device ids\r\n const deviceIPs: string[] = []; // to check for duplicate device IPs\r\n for (let i = 0; i < this.config.tableDevices.length; i++) {\r\n const lpDevice = this.config.tableDevices[i];\r\n const finalDevice: IDevice = {\r\n name: '',\r\n id: '',\r\n ip: '',\r\n enabled: false,\r\n mqttInfoObjectsCreated: false,\r\n mqttInfoKeys: [],\r\n restProtocol: 'http',\r\n restPort: 0,\r\n restPassword: '',\r\n lastSeen: 0, // timestamp\r\n isAlive: false,\r\n };\r\n\r\n // name\r\n if (this.isEmpty(lpDevice.name)) {\r\n this.log.error(`Provided device name \"${lpDevice.name}\" is empty!`);\r\n return false;\r\n }\r\n finalDevice.name = lpDevice.name.trim();\r\n\r\n // id\r\n finalDevice.id = this.cleanDeviceName(lpDevice.name);\r\n if (finalDevice.id.length < 1) {\r\n this.log.error(`Provided device name \"${lpDevice.name}\" is too short and/or has invalid characters!`);\r\n return false;\r\n }\r\n if (deviceIds.includes(finalDevice.id)) {\r\n this.log.error(`Device \"${finalDevice.name}\" -> id:\"${finalDevice.id}\" is used for more than once device.`);\r\n return false;\r\n } else {\r\n deviceIds.push(finalDevice.id);\r\n }\r\n\r\n // REST Protocol (http/https)\r\n if (lpDevice.restProtocol !== 'http' && lpDevice.restProtocol !== 'https') {\r\n this.log.warn(`${finalDevice.name}: REST API Protocol is empty, set to http as default.`);\r\n finalDevice.restProtocol = 'http';\r\n } else {\r\n finalDevice.restProtocol = lpDevice.restProtocol;\r\n }\r\n\r\n // IP Address\r\n if (!this.isIpAddressValid(lpDevice.ip)) {\r\n this.log.error(`${finalDevice.name}: Provided IP address \"${lpDevice.ip}\" is not valid!`);\r\n return false;\r\n }\r\n if (deviceIPs.includes(lpDevice.ip)) {\r\n this.log.error(`Device \"${finalDevice.name}\" -> IP:\"${lpDevice.ip}\" is used for more than once device.`);\r\n return false;\r\n } else {\r\n deviceIPs.push(lpDevice.ip);\r\n finalDevice.ip = lpDevice.ip;\r\n }\r\n\r\n // REST Port\r\n if (isNaN(lpDevice.restPort) || lpDevice.restPort < 0 || lpDevice.restPort > 65535) {\r\n this.log.error(`Adapter config Fully port number ${lpDevice.restPort} is not valid, should be >= 0 and < 65536.`);\r\n return false;\r\n } else {\r\n finalDevice.restPort = Math.round(lpDevice.restPort);\r\n }\r\n // REST Password\r\n if (isEmpty(lpDevice.restPassword)) {\r\n this.log.error(`Remote Admin (REST API) Password must not be empty!`);\r\n return false;\r\n } else {\r\n finalDevice.restPassword = lpDevice.restPassword;\r\n }\r\n\r\n // Enabled status\r\n finalDevice.enabled = lpDevice.enabled ? true : false;\r\n\r\n // Debug log of config\r\n const logConfig = { ...finalDevice }; // copy object using spread\r\n logConfig.restPassword = '(hidden)'; // do not show password in log !\r\n this.log.debug(`Final Config: ${JSON.stringify(logConfig)}`);\r\n\r\n // Finalize\r\n this.fullysAll[finalDevice.ip] = finalDevice;\r\n if (lpDevice.enabled) {\r\n this.fullysEnbl[finalDevice.ip] = finalDevice;\r\n this.log.info(`\uD83D\uDDF8 ${finalDevice.name} (${finalDevice.ip}): Config successfully verified.`);\r\n } else {\r\n this.fullysDisbl[finalDevice.ip] = finalDevice;\r\n this.log.info(`${finalDevice.name} (${finalDevice.ip}) is not enabled in settings, so it will not be used by adapter.`);\r\n }\r\n }\r\n\r\n if (Object.keys(this.fullysEnbl).length === 0) {\r\n this.log.error(`No active devices with correct configuration found.`);\r\n return false;\r\n }\r\n return true;\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * On Alive Changes\r\n * MQTT is being used only, REST API not.\r\n */\r\n public async onMqttAlive(ip: string, isAlive: true | false, msg: string): Promise {\r\n try {\r\n const prevIsAlive = this.fullysEnbl[ip].isAlive;\r\n this.fullysEnbl[ip].isAlive = isAlive;\r\n\r\n // Has this function ever been called before? If adapter is restarted, we ensure log, etc.\r\n const calledBefore = this.onMqttAlive_EverBeenCalledBefore; // Keep old value\r\n this.onMqttAlive_EverBeenCalledBefore = true; // Now it was called\r\n\r\n /***********\r\n * 1 - Fully Device\r\n ***********/\r\n // if alive status changed\r\n if ((!calledBefore && isAlive === true) || prevIsAlive !== isAlive) {\r\n // Set Device isAlive Status - we could also use setStateChanged()...\r\n this.setState(this.fullysEnbl[ip].id + '.alive', { val: isAlive, ack: true });\r\n\r\n // log\r\n if (isAlive) {\r\n this.log.info(`${this.fullysEnbl[ip].name} is alive (MQTT: ${msg})`);\r\n } else {\r\n this.log.warn(`${this.fullysEnbl[ip].name} is not alive! (MQTT: ${msg})`);\r\n }\r\n } else {\r\n // No change\r\n }\r\n\r\n /***********\r\n * 2 - Adapter Connection indicator\r\n ***********/\r\n let countAll = 0;\r\n let countAlive = 0;\r\n for (const lpIpAddr in this.fullysEnbl) {\r\n countAll++;\r\n if (this.fullysEnbl[lpIpAddr].isAlive) {\r\n countAlive++;\r\n }\r\n }\r\n let areAllAlive = false;\r\n if (countAll > 0 && countAll === countAlive) areAllAlive = true;\r\n this.setStateChanged('info.connection', { val: areAllAlive, ack: true });\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * MQTT: once new device info packet is coming in\r\n */\r\n public async onMqttInfo(obj: { clientId: string; ip: string; topic: string; infoObj: { [k: string]: any } }): Promise {\r\n try {\r\n // log\r\n this.log.debug(`[MQTT] ${this.fullysEnbl[obj.ip].name} published info, topic: ${obj.topic}`);\r\n //this.log.debug(`[MQTT] Client ${obj.ip} Publish Info: Details: ${JSON.stringify(obj.infoObj)}`);\r\n\r\n // Create info objects if not yet existing\r\n const formerInfoKeysLength: number = this.fullysEnbl[obj.ip].mqttInfoKeys.length;\r\n const newInfoKeysAdded: string[] = [];\r\n for (const key in obj.infoObj) {\r\n const val = obj.infoObj[key];\r\n const valType = typeof val;\r\n // only accept certain types\r\n if (valType !== 'string' && valType !== 'boolean' && valType !== 'object' && valType !== 'number') {\r\n this.log.warn(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Unknown type ${valType} of key '${key}' in info object`);\r\n continue;\r\n }\r\n // Create info object if not yet seen - this check is used for increasing performance by not unnesserily call setObjectNotExistsAsync() every time new info package comes in\r\n if (!this.fullysEnbl[obj.ip].mqttInfoKeys.includes(key)) {\r\n this.fullysEnbl[obj.ip].mqttInfoKeys.push(key);\r\n newInfoKeysAdded.push(key);\r\n await this.setObjectNotExistsAsync(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { type: 'state', common: { name: 'Info: ' + key, type: valType, role: 'value', read: true, write: false }, native: {} });\r\n }\r\n }\r\n if (formerInfoKeysLength === 0) this.log.debug(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Initially create states for ${newInfoKeysAdded.length} info items (if not yet existing)`);\r\n if (formerInfoKeysLength > 0 && newInfoKeysAdded.length > 0) this.log.info(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Created new info object(s) as not seen before (if object(s) did not exist): ${newInfoKeysAdded.join(', ')}`);\r\n\r\n // Set info objects\r\n for (const key in obj.infoObj) {\r\n const newVal = typeof obj.infoObj[key] === 'object' ? JSON.stringify(obj.infoObj[key]) : obj.infoObj[key]; // https://forum.iobroker.net/post/628870 - https://forum.iobroker.net/post/960260\r\n if (this.config.mqttUpdateUnchangedObjects) {\r\n this.setState(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { val: newVal, ack: true });\r\n } else {\r\n this.setStateChanged(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { val: newVal, ack: true });\r\n }\r\n }\r\n this.setState(this.fullysEnbl[obj.ip].id + '.lastInfoUpdate', { val: Date.now(), ack: true });\r\n this.setState(this.fullysEnbl[obj.ip].id + '.alive', { val: true, ack: true });\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * MQTT: once new event packet is coming in\r\n */\r\n public async onMqttEvent(obj: { clientId: string; ip: string; topic: string; cmd: string }): Promise {\r\n try {\r\n // log\r\n this.log.debug(`[MQTT] \uD83D\uDCE1 ${this.fullysEnbl[obj.ip].name} published event, topic: ${obj.topic}, cmd: ${obj.cmd}`);\r\n\r\n /**\r\n * Set Event State\r\n */\r\n const pthEvent = `${this.fullysEnbl[obj.ip].id}.Events.${obj.cmd}`;\r\n if (!(await this.getObjectAsync(pthEvent))) {\r\n this.log.debug(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Event ${obj.cmd} received but state ${pthEvent} does not exist, so we create it first`);\r\n await this.setObjectNotExistsAsync(pthEvent, { type: 'state', common: { name: 'Event: ' + obj.cmd, type: 'boolean', role: 'switch', read: true, write: false }, native: {} });\r\n }\r\n this.setState(pthEvent, { val: true, ack: true });\r\n\r\n /**\r\n * Confirm Command state(s) with ack: true\r\n */\r\n const pthCmd = this.fullysEnbl[obj.ip].id + '.Commands';\r\n\r\n // Check if it is a switch with MQTT commands connected\r\n const idx = this.getIndexFromConf(CONST.cmdsSwitches, ['mqttOn', 'mqttOff'], obj.cmd);\r\n if (idx !== -1) {\r\n // We have a switch\r\n const conf = CONST.cmdsSwitches[idx]; // the found line from config array\r\n const onOrOffCmd = obj.cmd === conf.mqttOn ? true : false;\r\n await this.setStateAsync(`${pthCmd}.${conf.id}`, { val: onOrOffCmd, ack: true });\r\n await this.setStateAsync(`${pthCmd}.${conf.cmdOn}`, { val: onOrOffCmd, ack: true });\r\n await this.setStateAsync(`${pthCmd}.${conf.cmdOff}`, { val: !onOrOffCmd, ack: true });\r\n } else {\r\n // No switch\r\n const idx = this.getIndexFromConf(CONST.cmds, ['id'], obj.cmd);\r\n if (idx !== -1 && CONST.cmds[idx].type === 'boolean') {\r\n // We have a button, so set it to true\r\n await this.setStateAsync(`${pthCmd}.${obj.cmd}`, { val: true, ack: true });\r\n } else {\r\n this.log.silly(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Event cmd ${obj.cmd} - no REST API command is existing, so skip confirmation with with ack:true`);\r\n }\r\n }\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * Called once a subscribed state changes.\r\n * Ready once subscribeStatesAsync() is called...\r\n * @param id - e.g. \"fully-mqtt.0.Tablet-Bathroom.Commands.screenSwitch\"\r\n * @param stateObj - e.g. { val: true, ack: false, ts: 123456789, q: 0, lc: 123456789 }\r\n */\r\n private async onStateChange(stateId: string, stateObj: ioBroker.State | null | undefined): Promise {\r\n try {\r\n if (!stateObj) return; // state was deleted, we disregard...\r\n if (stateObj.ack) return; // ignore ack:true\r\n const idSplit = stateId.split('.');\r\n const deviceId = idSplit[2]; // \"Tablet-Bathroom\"\r\n const channel = idSplit[3]; // \"Commands\"\r\n const cmd = idSplit[4]; // \"screenSwitch\"\r\n const pth = deviceId + '.' + channel; // Tablet-Bathroom.Commands\r\n /**\r\n * Commands\r\n */\r\n if (channel === 'Commands') {\r\n this.log.debug(`state ${stateId} changed: ${stateObj.val} (ack = ${stateObj.ack})`);\r\n // Get device object\r\n const fully = this.getFullyByKey('id', deviceId);\r\n if (!fully) throw `Fully object for deviceId '${deviceId}' not found!`;\r\n\r\n let cmdToSend: string | undefined = cmd; // Command to send to Fully\r\n let switchConf: undefined | ICmds = undefined; // Config line of switch\r\n\r\n /****************\r\n * Check if it is a switch state cmd, like 'screenSwitch'\r\n ****************/\r\n const idxSw = this.getIndexFromConf(CONST.cmdsSwitches, ['id'], cmd);\r\n if (idxSw !== -1) {\r\n // It is a switch\r\n switchConf = CONST.cmdsSwitches[idxSw]; // the found line from config array\r\n cmdToSend = stateObj.val ? switchConf.cmdOn : switchConf.cmdOff;\r\n } else {\r\n // Not a switch.\r\n // If val is false, we disregard, since it is a button only\r\n if (!stateObj.val) return;\r\n }\r\n if (!cmdToSend) throw `onStateChange() - ${stateId}: fullyCmd could not be determined!`;\r\n\r\n /**\r\n * Send Command\r\n */\r\n const sendCommand = await this.restApi_inst.sendCmd(fully, cmdToSend, stateObj.val);\r\n if (sendCommand) {\r\n if (this.config.restCommandLogAsDebug) {\r\n this.log.debug(`\uD83D\uDDF8 ${fully.name}: Command ${cmd} successfully set to ${stateObj.val}`);\r\n } else {\r\n this.log.info(`\uD83D\uDDF8 ${fully.name}: Command ${cmd} successfully set to ${stateObj.val}`);\r\n }\r\n /**\r\n * Confirm with ack:true\r\n */\r\n if (switchConf !== undefined) {\r\n // it is a switch\r\n const onOrOffCmdVal = cmd === switchConf.cmdOn ? true : false;\r\n await this.setStateAsync(`${pth}.${switchConf.id}`, { val: onOrOffCmdVal, ack: true });\r\n await this.setStateAsync(`${pth}.${switchConf.cmdOn}`, { val: onOrOffCmdVal, ack: true });\r\n await this.setStateAsync(`${pth}.${switchConf.cmdOff}`, { val: !onOrOffCmdVal, ack: true });\r\n } else {\r\n // No switch\r\n if (typeof stateObj.val === 'boolean') {\r\n const idx = this.getIndexFromConf(CONST.cmds, ['id'], cmd);\r\n if (idx !== -1) {\r\n if (CONST.cmds[idx].type === 'boolean') {\r\n // Is a button\r\n await this.setStateAsync(stateId, { val: true, ack: true });\r\n } else {\r\n // This should actually not happen, as we just define buttons in commands, but anyway\r\n this.log.warn(`${fully.name}: ${stateId} - val: ${stateObj.val} is boolean, but cmd ${cmd} is not defined in CONF`);\r\n await this.setStateAsync(stateId, { val: stateObj.val, ack: true });\r\n }\r\n } else {\r\n this.log.warn(`${fully.name}: ${stateId} - val: ${stateObj.val}, cmd ${cmd} is not defined in CONF`);\r\n }\r\n } else {\r\n // Non-boolean, so just set val with ack:true...\r\n await this.setStateAsync(stateId, { val: stateObj.val, ack: true });\r\n }\r\n }\r\n } else {\r\n // log, more log lines were already published by this.restApi_inst.sendCmd()\r\n this.log.debug(`${fully.name}: restApiSendCmd() was not successful (${stateId})`);\r\n }\r\n }\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * Get Fully Object per provided key and value\r\n * {\r\n * '192.168.10.20': {name: 'Tablet Kitchen', id:'Tablet-Kitchen', ip:'192.168.10.20', ...},\r\n * '192.168.10.30': {name: 'Tablet Hallway', id:'Tablet-Hallway', ip:'192.168.10.30', ...},\r\n * }\r\n * getFullyByKey('id', 'Tablet-Hallway') will return the second object...\r\n * @param keyId - e.g. 'id', 'name', ...\r\n * @param value - e.g. 'Tablet Hallway', ...\r\n * @returns - fully object or false if not found\r\n */\r\n private getFullyByKey(keyId: string, value: any): IDevice | false {\r\n for (const ip in this.fullysEnbl) {\r\n if (keyId in this.fullysEnbl[ip]) {\r\n const lpKeyId = keyId as string;\r\n // Wow, what a line. Due to: https://bobbyhadz.com/blog/typescript-element-implicitly-has-any-type-expression\r\n const lpVal = this.fullysEnbl[ip][lpKeyId as keyof (typeof this.fullysEnbl)[typeof ip]];\r\n if (lpVal === value) {\r\n return this.fullysEnbl[ip];\r\n }\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * Gets Index for given keys and a value\r\n * @param config - config like CONST.cmds\r\n * @param keys - like ['mqttOn','mqttOff']\r\n * @param cmd - like 'onScreensaverStart'\r\n * @returns Index (0-...), or -1 if not found\r\n */\r\n private getIndexFromConf(config: { [k: string]: any }[], keys: string[], cmd: string): number {\r\n try {\r\n let index = -1;\r\n for (const key of keys) {\r\n // Get array index\r\n index = config.findIndex((x: { [k: string]: any }) => x[key] === cmd);\r\n if (index !== -1) break;\r\n }\r\n return index;\r\n } catch (e) {\r\n this.log.error(this.err2Str(e));\r\n return -1;\r\n }\r\n }\r\n\r\n /**\r\n * Is called when adapter shuts down - callback has to be called under any circumstances!\r\n */\r\n private async onUnload(callback: () => void): Promise {\r\n try {\r\n // All Fullys: Set alive status to null\r\n if (this.fullysAll) {\r\n for (const ip in this.fullysAll) {\r\n // We check first if object exists, as there were errors in log on when updating adpater via Github (related to missing objects)\r\n if (await this.getObjectAsync(this.fullysAll[ip].id)) {\r\n this.setState(this.fullysAll[ip].id + '.alive', { val: null, ack: true });\r\n }\r\n }\r\n }\r\n\r\n // Clear MQTT server timeouts\r\n if (this.mqtt_Server) {\r\n for (const clientId in this.mqtt_Server.devices) {\r\n // @ts-expect-error \"Type 'null' is not assignable to type 'Timeout'.ts(2345)\" - we check for not being null via \"if\"\r\n if (this.mqtt_Server.devices[clientId].timeoutNoUpdate) this.clearTimeout(this.mqtt_Server.devices[clientId].timeoutNoUpdate);\r\n }\r\n }\r\n\r\n // destroy MQTT Server\r\n if (this.mqtt_Server) {\r\n this.mqtt_Server.terminate();\r\n }\r\n\r\n callback();\r\n } catch (e) {\r\n callback();\r\n }\r\n }\r\n}\r\n\r\nif (require.main !== module) {\r\n // Export the constructor in compact mode\r\n module.exports = (options: Partial | undefined) => new FullyMqtt(options);\r\n} else {\r\n // otherwise start the instance directly\r\n (() => new FullyMqtt())();\r\n}\r\n"], "mappings": "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", "names": ["idx"] }