diff --git a/lib/axiosCommand.js b/lib/axiosCommand.js deleted file mode 100644 index a761577..0000000 --- a/lib/axiosCommand.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const axios = require('axios'); - -class axiosCommand { - constructor(adapter) { - - this.devices = {}; - } - - start() { - - } - - -} -module.exports = axiosCommand; - diff --git a/lib/constants.js b/lib/constants.js index f6acde0..b007973 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,41 +1,41 @@ 'use strict'; const IConst = { - mqttEvents: ["background", "foreground", "screenOn", "screenOff", "pluggedAC", "pluggedUSB", "pluggedWireless", "unplugged", "networkReconnect", "networkDisconnect", "internetReconnect", "internetDisconnect", "powerOn", "powerOff", "showKeyboard", "hideKeyboard", "onMotion", "onDarkness", "onMovement", "volumeUp", "volumeDown", "onQrScanCancelled", "onBatteryLevelChanged", "onScreensaverStart", "onScreensaverStop", "onDaydreamStart", "onDaydreamStop", "onItemPlay", "onPlaylistPlay", "facesDetected"], - cmdsSwitches: [ - { id: "screenSwitch", name: "Turn Screen on and off", type: "boolean", cmdOn: "screenOn", cmdOff: "screenOff", mqttOn: "screenOn", mqttOff: "screenOff" }, - { id: "screensaverSwitch", name: "Turn Screensaver on and off", type: "boolean", cmdOn: "startScreensaver", cmdOff: "stopScreensaver", mqttOn: "onScreensaverStart", mqttOff: "onScreensaverStop" }, - { id: "daydreamSwitch", name: "Turn Daydream on and off", type: "boolean", cmdOn: "startDaydream", cmdOff: "stopDaydream", mqttOn: "onDaydreamStart", mqttOff: "onDaydreamStop" }, - { id: "lockedModeSwitch", name: "Turn Locked Mode on and off", type: "boolean", cmdOn: "enableLockedMode", cmdOff: "disableLockedMode" }, - { id: "isInForeground", name: "Bring Fully in foreground or background", type: "boolean", cmdOn: "toForeground", cmdOff: "toBackground", mqttOn: "foreground", mqttOff: "background" } - ], - cmds: [ - { id: "clearCache", name: "Clear Cache", type: "boolean" }, - { id: "clearCookies", name: "Clear Cookies", type: "boolean" }, - { id: "clearWebstorage", name: "Clear Webstorage", type: "boolean" }, - { id: "disableLockedMode", name: "Disable Locked Mode", type: "boolean" }, - { id: "enableLockedMode", name: "Enable Locked Mode", type: "boolean" }, - { id: "exitApp", name: "Exit App", type: "boolean" }, - { id: "forceSleep", name: "Force Sleep", type: "boolean" }, - { id: "loadStartURL", name: "Load Start URL", type: "boolean" }, - { id: "popFragment", name: "Pop Fragment", type: "boolean" }, - { id: "restartApp", name: "Restart App", type: "boolean" }, - { id: "screenOff", name: "Screen Off", type: "boolean" }, - { id: "screenOn", name: "Screen On", type: "boolean" }, - { id: "startDaydream", name: "Start Daydream", type: "boolean" }, - { id: "startScreensaver", name: "Start Screensaver", type: "boolean" }, - { id: "stopDaydream", name: "Stop Daydream", type: "boolean" }, - { id: "stopScreensaver", name: "Stop Screensaver", type: "boolean" }, - { id: "toBackground", name: "Bring Fully to Background", type: "boolean" }, - { id: "toForeground", name: "Bring Fully to Foreground", type: "boolean" }, - { id: "triggerMotion", name: "Trigger Motion", type: "boolean" }, - { id: "loadURL", name: "Load URL", type: "string" }, - { id: "setStringSetting", name: "Set String Setting", type: "string" }, - { id: "startApplication", name: "Start Application", type: "string" }, - { id: "textToSpeech", name: "Text To Speech", type: "string" }, - { id: "screenBrightness", name: "Screen Brightness", type: "number" }, - { id: "setAudioVolume", name: "Audio Volume", type: "number" } - ] + mqttEvents: ['background', 'foreground', 'screenOn', 'screenOff', 'pluggedAC', 'pluggedUSB', 'pluggedWireless', 'unplugged', 'networkReconnect', 'networkDisconnect', 'internetReconnect', 'internetDisconnect', 'powerOn', 'powerOff', 'showKeyboard', 'hideKeyboard', 'onMotion', 'onDarkness', 'onMovement', 'volumeUp', 'volumeDown', 'onQrScanCancelled', 'onBatteryLevelChanged', 'onScreensaverStart', 'onScreensaverStop', 'onDaydreamStart', 'onDaydreamStop', 'onItemPlay', 'onPlaylistPlay', 'facesDetected'], + cmdsSwitches: [ + { id: 'screenSwitch', name: 'Turn Screen on and off', type: 'boolean', cmdOn: 'screenOn', cmdOff: 'screenOff', mqttOn: 'screenOn', mqttOff: 'screenOff' }, + { id: 'screensaverSwitch', name: 'Turn Screensaver on and off', type: 'boolean', cmdOn: 'startScreensaver', cmdOff: 'stopScreensaver', mqttOn: 'onScreensaverStart', mqttOff: 'onScreensaverStop' }, + { id: 'daydreamSwitch', name: 'Turn Daydream on and off', type: 'boolean', cmdOn: 'startDaydream', cmdOff: 'stopDaydream', mqttOn: 'onDaydreamStart', mqttOff: 'onDaydreamStop' }, + { id: 'lockedModeSwitch', name: 'Turn Locked Mode on and off', type: 'boolean', cmdOn: 'enableLockedMode', cmdOff: 'disableLockedMode' }, + { id: 'isInForeground', name: 'Bring Fully in foreground or background', type: 'boolean', cmdOn: 'toForeground', cmdOff: 'toBackground', mqttOn: 'foreground', mqttOff: 'background' } + ], + cmds: [ + { id: 'clearCache', name: 'Clear Cache', type: 'boolean' }, + { id: 'clearCookies', name: 'Clear Cookies', type: 'boolean' }, + { id: 'clearWebstorage', name: 'Clear Webstorage', type: 'boolean' }, + { id: 'disableLockedMode', name: 'Disable Locked Mode', type: 'boolean' }, + { id: 'enableLockedMode', name: 'Enable Locked Mode', type: 'boolean' }, + { id: 'exitApp', name: 'Exit App', type: 'boolean' }, + { id: 'forceSleep', name: 'Force Sleep', type: 'boolean' }, + { id: 'loadStartURL', name: 'Load Start URL', type: 'boolean' }, + { id: 'popFragment', name: 'Pop Fragment', type: 'boolean' }, + { id: 'restartApp', name: 'Restart App', type: 'boolean' }, + { id: 'screenOff', name: 'Screen Off', type: 'boolean' }, + { id: 'screenOn', name: 'Screen On', type: 'boolean' }, + { id: 'startDaydream', name: 'Start Daydream', type: 'boolean' }, + { id: 'startScreensaver', name: 'Start Screensaver', type: 'boolean' }, + { id: 'stopDaydream', name: 'Stop Daydream', type: 'boolean' }, + { id: 'stopScreensaver', name: 'Stop Screensaver', type: 'boolean' }, + { id: 'toBackground', name: 'Bring Fully to Background', type: 'boolean' }, + { id: 'toForeground', name: 'Bring Fully to Foreground', type: 'boolean' }, + { id: 'triggerMotion', name: 'Trigger Motion', type: 'boolean' }, + { id: 'loadURL', name: 'Load URL', type: 'string' }, + { id: 'setStringSetting', name: 'Set String Setting', type: 'string' }, + { id: 'startApplication', name: 'Start Application', type: 'string' }, + { id: 'textToSpeech', name: 'Text To Speech', type: 'string' }, + { id: 'screenBrightness', name: 'Screen Brightness', type: 'number' }, + { id: 'setAudioVolume', name: 'Audio Volume', type: 'number' } + ] }; module.exports = IConst; diff --git a/lib/methods.js b/lib/methods.js index 946f2b4..bc27986 100644 --- a/lib/methods.js +++ b/lib/methods.js @@ -1,77 +1,77 @@ 'use strict'; function err2Str(error) { - if (error instanceof Error) { - if (error.stack) - return error.stack; - if (error.message) - return error.message; - return JSON.stringify(error); - } else { - if (typeof error === "string") - return error; - return JSON.stringify(error); - } + if (error instanceof Error) { + if (error.stack) + return error.stack; + if (error.message) + return error.message; + return JSON.stringify(error); + } else { + if (typeof error === 'string') + return error; + return JSON.stringify(error); + } } function cleanDeviceName(str) { - let res = str.replace(this.FORBIDDEN_CHARS, ""); - res = res.replace(/\./g, ""); - res = res.replace(/\s{2,}/g, " "); - res = res.trim(); - res = res.replace(/\s/g, "_"); - if (res.replace(/_/g, "").length === 0) - res = ""; - return res; + let res = str.replace(this.FORBIDDEN_CHARS, ''); + res = res.replace(/\./g, ''); + res = res.replace(/\s{2,}/g, ' '); + res = res.trim(); + res = res.replace(/\s/g, '_'); + if (res.replace(/_/g, '').length === 0) + res = ''; + return res; } function isIpAddressValid(ip) { - 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]?)$/; - if (pattern.test(ip)) { - return true; - } else { - return false; - } + 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]?)$/; + if (pattern.test(ip)) { + return true; + } else { + return false; + } } function getConfigValuePerKey(config, key1, key1Value, key2) { - for (const lpConfDevice of config) { - if (lpConfDevice[key1] === key1Value) { - if (lpConfDevice[key2] === void 0) { - return -1; - } else { - return lpConfDevice[key2]; - } + for (const lpConfDevice of config) { + if (lpConfDevice[key1] === key1Value) { + if (lpConfDevice[key2] === void 0) { + return -1; + } else { + return lpConfDevice[key2]; + } + } } - } - return -1; + return -1; } function isEmpty(toCheck) { - if (toCheck === null || typeof toCheck === "undefined") - return true; - if (typeof toCheck === "function") - return false; - let x = JSON.stringify(toCheck); - x = x.replace(/\s+/g, ""); - x = x.replace(/"+/g, ""); - x = x.replace(/'+/g, ""); - x = x.replace(/\[+/g, ""); - x = x.replace(/\]+/g, ""); - x = x.replace(/\{+/g, ""); - x = x.replace(/\}+/g, ""); - return x === "" ? true : false; + if (toCheck === null || typeof toCheck === 'undefined') + return true; + if (typeof toCheck === 'function') + return false; + let x = JSON.stringify(toCheck); + x = x.replace(/\s+/g, ''); + x = x.replace(/"+/g, ''); + x = x.replace(/'+/g, ''); + x = x.replace(/\[+/g, ''); + x = x.replace(/\]+/g, ''); + x = x.replace(/\{+/g, ''); + x = x.replace(/\}+/g, ''); + return x === '' ? true : false; } async function wait(ms) { - try { - await new Promise((w) => setTimeout(w, ms)); - } catch (e) { - this.log.error(this.err2Str(e)); - return; - } + try { + await new Promise((w) => setTimeout(w, ms)); + } catch (e) { + this.log.error(this.err2Str(e)); + return; + } } module.exports = { - cleanDeviceName, - err2Str, - getConfigValuePerKey, - isEmpty, - isIpAddressValid, - wait + cleanDeviceName, + err2Str, + getConfigValuePerKey, + isEmpty, + isIpAddressValid, + wait }; diff --git a/lib/mqtt-server.js b/lib/mqtt-server.js index a00c0c2..1f49791 100644 --- a/lib/mqtt-server.js +++ b/lib/mqtt-server.js @@ -1,330 +1,330 @@ 'use strict'; -const Aedes = require("aedes"); -const net = require("net"); +const Aedes = require('aedes'); +const net = require('net'); class MqttServer { - constructor(adapter) { - this.port = -1; - this.notAuthorizedClients = []; - this.adapter = adapter; - this.aedes = new Aedes(); - this.server = net.createServer(this.aedes.handle); - - this.devices = {}; // key = MQTT Client ID, property: IMqttDevice - } - start() { - try { - this.port = this.adapter.config.mqttPort; - - this.server.listen(this.port, () => { - this.adapter.log.info(`\u{1F680} MQTT Server started and is listening on port ${this.port}.`); - }); - - this.aedes.authenticate = (client, username, password, callback) => { + constructor(adapter) { + this.port = -1; + this.notAuthorizedClients = []; + this.adapter = adapter; + this.aedes = new Aedes(); + this.server = net.createServer(this.aedes.handle); + + this.devices = {}; // key = MQTT Client ID, property: IMqttDevice + } + start() { try { - if (this.notAuthorizedClients.includes(client.id)) { - callback(null, false); - return; - } + this.port = this.adapter.config.mqttPort; + + this.server.listen(this.port, () => { + this.adapter.log.info(`\u{1F680} MQTT Server started and is listening on port ${this.port}.`); + }); + + this.aedes.authenticate = (client, username, password, callback) => { + try { + if (this.notAuthorizedClients.includes(client.id)) { + callback(null, false); + return; + } - // Create device entry with id as key, if not yet existing - if (!this.devices[client.id]) this.devices[client.id] = {}; + // Create device entry with id as key, if not yet existing + if (!this.devices[client.id]) this.devices[client.id] = {}; - /** + /** * Get IP * This rather complicated way is needed, see https://github.com/moscajs/aedes/issues/186 * 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 */ - let ip = undefined; - - if (client.conn && "remoteAddress" in client.conn && typeof client.conn.remoteAddress === "string") { - const ipSource = client.conn.remoteAddress; // like: ::ffff:192.168.1.213 - this.adapter.log.debug(`[MQTT] client.conn.remoteAddress = "${ipSource}" - ${client.id}`); - - ip = ipSource.substring(ipSource.lastIndexOf(":") + 1); - - if (!this.adapter.isIpAddressValid(ip)) - ip = undefined; - } - - if (ip && !Object.keys(this.adapter.fullysMQTT).includes(ip)) { - this.adapter.log.info(`[MQTT] Client ${client.id} not authorized: ${ip} is not an active Fully MQTT device IP per adapter settings.`); - this.notAuthorizedClients.push(client.id); - callback(null, false); - return; - } - - const ipMsg = ip ? `${this.adapter.fullysMQTT[ip].name} (${ip})` : `${client.id} (IP unknown)`; - this.adapter.log.debug(`[MQTT] Client ${ipMsg} trys to authenticate...`); - - if (ip) this.devices[client.id].ip = ip; - - if (!this.adapter.config.mqttDoNotVerifyUserPw) { - if (username !== this.adapter.config.mqttUser) { - this.adapter.log.warn(`MQTT Client ${ipMsg} Authorization rejected: received user name '${username}' does not match '${this.adapter.config.mqttUser}' in adapter settings.`); - callback(null, false); - return; - } - if (password.toString() !== this.adapter.config.mqttPassword) { - this.adapter.log.warn(`MQTT Client ${ipMsg} Authorization rejected: received password does not match with password in adapter settings.`); - callback(null, false); - return; - } - } - - this.adapter.log.info(`\u{1F511} MQTT Client ${ipMsg} successfully authenticated.`); - callback(null, true); - } catch (e) { - this.adapter.log.error(this.adapter.err2Str(e)); - callback(null, false); - } - }; - - this.aedes.on("client", (client) => { - try { - if (!client) - return; - - if (!this.devices[client.id]) - this.devices[client.id] = {}; - - const ip = this.devices[client.id].ip; - const ipMsg = ip ? `${this.adapter.fullysMQTT[ip].name} (${ip})` : `${client.id} (IP unknown)`; - this.adapter.log.debug(`[MQTT] Client ${ipMsg} connected to broker ${this.aedes.id}`); - this.adapter.log.info(`\u{1F517} MQTT Client ${ipMsg} successfully connected.`); - this.setIsAlive(client.id, true, "client connected"); - this.scheduleCheckIfStillActive(client.id); - } catch (e) { - this.adapter.log.error(this.adapter.err2Str(e)); - return; - } - }); - - this.aedes.on("publish", (packet, client) => { - try { - if (!client || !packet) - return; - - this.setIsAlive(client.id, true, "client published message"); - if (!this.devices[client.id]) - this.devices[client.id] = {}; - if (packet.qos !== 1) - return; - if (packet.retain) { - const info = JSON.parse(packet.payload.toString()); - if (!("startUrl" in info) && !("ip4" in info)) { - this.adapter.log.error(`[MQTT] Packet rejected: ${info.ip4} - Info packet expected, but ip4 and startUrl is not defined in packet. ${info.deviceId}`); - return; - } - const ip = info.ip4; - const devMsg = `${this.adapter.fullysMQTT[ip].name} (${ip})`; - if (!Object.keys(this.adapter.fullysMQTT).includes(ip)) { - this.adapter.log.error(`[MQTT] Client ${devMsg} Packet rejected: IP is not allowed per adapter settings. ${client.id}`); - return; - } - this.devices[client.id].ip = ip; - - const prevTime = this.devices[client.id].previousInfoPublishTime; - const limit = this.adapter.config.mqttPublishedInfoDelay * 1000; // milliseconds - if (prevTime && prevTime !== 0) { - if (Date.now() - prevTime < limit) { - const diffMs = Date.now() - prevTime; - this.adapter.log.silly(`[MQTT] ${devMsg} Packet rejected: Last packet came in ${diffMs}ms (${Math.round(diffMs / 1000)}s) ago...`); - return; - } - } - this.devices[client.id].previousInfoPublishTime = Date.now(); - if (!this.devices[client.id].mqttFirstReceived) { - this.adapter.log.debug(`[MQTT] Client ${client.id} = ${this.adapter.fullysMQTT[ip].name} = ${ip}`); - this.devices[client.id].mqttFirstReceived = true; - } - const result = { - clientId: client.id, - ip: ip, - topic: packet.topic, - infoObj: info + let ip = undefined; + + if (client.conn && 'remoteAddress' in client.conn && typeof client.conn.remoteAddress === 'string') { + const ipSource = client.conn.remoteAddress; // like: ::ffff:192.168.1.213 + this.adapter.log.debug(`[MQTT] client.conn.remoteAddress = "${ipSource}" - ${client.id}`); + + ip = ipSource.substring(ipSource.lastIndexOf(':') + 1); + + if (!this.adapter.isIpAddressValid(ip)) + ip = undefined; + } + + if (ip && !Object.keys(this.adapter.fullysMQTT).includes(ip)) { + this.adapter.log.info(`[MQTT] Client ${client.id} not authorized: ${ip} is not an active Fully MQTT device IP per adapter settings.`); + this.notAuthorizedClients.push(client.id); + callback(null, false); + return; + } + + const ipMsg = ip ? `${this.adapter.fullysMQTT[ip].name} (${ip})` : `${client.id} (IP unknown)`; + this.adapter.log.debug(`[MQTT] Client ${ipMsg} trys to authenticate...`); + + if (ip) this.devices[client.id].ip = ip; + + if (!this.adapter.config.mqttDoNotVerifyUserPw) { + if (username !== this.adapter.config.mqttUser) { + this.adapter.log.warn(`MQTT Client ${ipMsg} Authorization rejected: received user name '${username}' does not match '${this.adapter.config.mqttUser}' in adapter settings.`); + callback(null, false); + return; + } + if (password.toString() !== this.adapter.config.mqttPassword) { + this.adapter.log.warn(`MQTT Client ${ipMsg} Authorization rejected: received password does not match with password in adapter settings.`); + callback(null, false); + return; + } + } + + this.adapter.log.info(`\u{1F511} MQTT Client ${ipMsg} successfully authenticated.`); + callback(null, true); + } catch (e) { + this.adapter.log.error(this.adapter.err2Str(e)); + callback(null, false); + } }; - this.adapter.onMqttInfo(result); - - } else if (packet.qos === 1 && !packet.retain) { - /** + this.aedes.on('client', (client) => { + try { + if (!client) + return; + + if (!this.devices[client.id]) + this.devices[client.id] = {}; + + const ip = this.devices[client.id].ip; + const ipMsg = ip ? `${this.adapter.fullysMQTT[ip].name} (${ip})` : `${client.id} (IP unknown)`; + this.adapter.log.debug(`[MQTT] Client ${ipMsg} connected to broker ${this.aedes.id}`); + this.adapter.log.info(`\u{1F517} MQTT Client ${ipMsg} successfully connected.`); + this.setIsAlive(client.id, true, 'client connected'); + this.scheduleCheckIfStillActive(client.id); + } catch (e) { + this.adapter.log.error(this.adapter.err2Str(e)); + return; + } + }); + + this.aedes.on('publish', (packet, client) => { + try { + if (!client || !packet) + return; + + this.setIsAlive(client.id, true, 'client published message'); + if (!this.devices[client.id]) + this.devices[client.id] = {}; + if (packet.qos !== 1) + return; + if (packet.retain) { + const info = JSON.parse(packet.payload.toString()); + if (!('startUrl' in info) && !('ip4' in info)) { + this.adapter.log.error(`[MQTT] Packet rejected: ${info.ip4} - Info packet expected, but ip4 and startUrl is not defined in packet. ${info.deviceId}`); + return; + } + const ip = info.ip4; + const devMsg = `${this.adapter.fullysMQTT[ip].name} (${ip})`; + if (!Object.keys(this.adapter.fullysMQTT).includes(ip)) { + this.adapter.log.error(`[MQTT] Client ${devMsg} Packet rejected: IP is not allowed per adapter settings. ${client.id}`); + return; + } + this.devices[client.id].ip = ip; + + const prevTime = this.devices[client.id].previousInfoPublishTime; + const limit = this.adapter.config.mqttPublishedInfoDelay * 1000; // milliseconds + if (prevTime && prevTime !== 0) { + if (Date.now() - prevTime < limit) { + const diffMs = Date.now() - prevTime; + this.adapter.log.silly(`[MQTT] ${devMsg} Packet rejected: Last packet came in ${diffMs}ms (${Math.round(diffMs / 1000)}s) ago...`); + return; + } + } + this.devices[client.id].previousInfoPublishTime = Date.now(); + if (!this.devices[client.id].mqttFirstReceived) { + this.adapter.log.debug(`[MQTT] Client ${client.id} = ${this.adapter.fullysMQTT[ip].name} = ${ip}`); + this.devices[client.id].mqttFirstReceived = true; + } + const result = { + clientId: client.id, + ip: ip, + topic: packet.topic, + infoObj: info + }; + + this.adapter.onMqttInfo(result); + + } else if (packet.qos === 1 && !packet.retain) { + /** * Event coming in... * Per fully documentation: Events will be published as fully/event/[eventId]/[deviceId] topic (non-retaining, QOS=1). */ - // {"deviceId":"xxxxxxxx-xxxxxxxx","event":"screenOn"} - // NOTE: Device ID is different to client id, we actually disregard deviceId - const msg = JSON.parse(packet.payload.toString()); - - if (!("event" in msg)) { - this.adapter.log.error(`[MQTT] Packet rejected: Event packet expected, but event is not defined in packet. ${client.id}`); - return; - } - if (msg.event === "mqttConnected") { - this.adapter.log.silly(`[MQTT] Client Publish Event: Disregard mqttConnected event - ${msg.deviceId}`); - return; - } - if (!this.devices[client.id]) { - this.adapter.log.info(`[MQTT] Client Publish Event: Device ID and according IP not yet seen thru "Publish Info"`); - this.adapter.log.info(`[MQTT] We wait until first info is published. ${msg.deviceId}`); - return; - } - const ip = this.devices[client.id].ip ? this.devices[client.id].ip : ""; - if (ip === "" || typeof ip !== "string") { - this.adapter.log.debug(`[MQTT] Client Publish Event: IP address could not be determined. - Client ID: ${client.id}`); - this.adapter.log.debug(`[MQTT] Please be patient until first MQTT info packet coming in (takes up to 1 minute)`); - return; - } - const result = { - clientId: client.id, - ip, - topic: packet.topic, - cmd: msg.event - }; - if (!this.devices[client.id].mqttFirstReceived) { - this.adapter.log.info(`[MQTT] \u{1F517} Client ${client.id} = ${this.adapter.fullysMQTT[ip].name} (${ip})`); - this.devices[client.id].mqttFirstReceived = true; - } - this.adapter.onMqttEvent(result); - } else { - return; - } + // {"deviceId":"xxxxxxxx-xxxxxxxx","event":"screenOn"} + // NOTE: Device ID is different to client id, we actually disregard deviceId + const msg = JSON.parse(packet.payload.toString()); + + if (!('event' in msg)) { + this.adapter.log.error(`[MQTT] Packet rejected: Event packet expected, but event is not defined in packet. ${client.id}`); + return; + } + if (msg.event === 'mqttConnected') { + this.adapter.log.silly(`[MQTT] Client Publish Event: Disregard mqttConnected event - ${msg.deviceId}`); + return; + } + if (!this.devices[client.id]) { + this.adapter.log.info(`[MQTT] Client Publish Event: Device ID and according IP not yet seen thru "Publish Info"`); + this.adapter.log.info(`[MQTT] We wait until first info is published. ${msg.deviceId}`); + return; + } + const ip = this.devices[client.id].ip ? this.devices[client.id].ip : ''; + if (ip === '' || typeof ip !== 'string') { + this.adapter.log.debug(`[MQTT] Client Publish Event: IP address could not be determined. - Client ID: ${client.id}`); + this.adapter.log.debug(`[MQTT] Please be patient until first MQTT info packet coming in (takes up to 1 minute)`); + return; + } + const result = { + clientId: client.id, + ip, + topic: packet.topic, + cmd: msg.event + }; + if (!this.devices[client.id].mqttFirstReceived) { + this.adapter.log.info(`[MQTT] \u{1F517} Client ${client.id} = ${this.adapter.fullysMQTT[ip].name} (${ip})`); + this.devices[client.id].mqttFirstReceived = true; + } + this.adapter.onMqttEvent(result); + } else { + return; + } + } catch (e) { + this.adapter.log.error(this.adapter.err2Str(e)); + return; + } + }); + this.aedes.on('clientDisconnect', (client) => { + const ip = this.devices[client.id].ip; + const logMsgName = ip ? this.adapter.fullysMQTT[ip].name : client.id; + if (this.adapter.config.mqttConnErrorsAsInfo) { + this.adapter.log.info(`MQTT Client ${logMsgName} disconnected.`); + } else { + this.adapter.log.error(`[MQTT] Client ${logMsgName} disconnected.`); + } + this.setIsAlive(client.id, false, 'client disconnected'); + }); + this.aedes.on('clientError', (client, e) => { + if (this.notAuthorizedClients.includes(client.id)) + return; + const ip = this.devices[client.id].ip; + const logMsgName = ip ? this.adapter.fullysMQTT[ip].name : client.id; + if (this.adapter.config.mqttConnErrorsAsInfo) { + this.adapter.log.info(`[MQTT] ${logMsgName}: Client error - ${e.message}`); + } else { + this.adapter.log.error(`[MQTT]\u{1F525} ${logMsgName}: Client error - ${e.message}`); + } + this.adapter.log.debug(`[MQTT]\u{1F525} ${logMsgName}: Client error - stack: ${e.stack}`); + this.setIsAlive(client.id, false, 'client error'); + }); + this.aedes.on('connectionError', (client, e) => { + const ip = this.devices[client.id].ip; + const logMsgName = ip ? this.adapter.fullysMQTT[ip].name : client.id; + if (this.adapter.config.mqttConnErrorsAsInfo) { + this.adapter.log.info(`[MQTT] ${logMsgName}: Connection error - ${e.message}`); + } else { + this.adapter.log.error(`[MQTT]\u{1F525} ${logMsgName}: Connection error - ${e.message}`); + } + this.adapter.log.debug(`[MQTT]\u{1F525} ${logMsgName}: Connection error - stack: ${e.stack}`); + this.setIsAlive(client.id, false, 'connection error'); + }); + this.server.on('error', (e) => { + if (e instanceof Error && e.message.startsWith('listen EADDRINUSE')) { + this.adapter.log.debug(`[MQTT] Cannot start server - ${e.message}`); + this.adapter.log.error(`[MQTT]\u{1F525} Cannot start server - Port ${this.port} is already in use. Try a different port!`); + } else { + this.adapter.log.error(`[MQTT]\u{1F525} Cannot start server - ${e.message}`); + } + this.terminate(); + }); } catch (e) { - this.adapter.log.error(this.adapter.err2Str(e)); - return; - } - }); - this.aedes.on("clientDisconnect", (client) => { - const ip = this.devices[client.id].ip; - const logMsgName = ip ? this.adapter.fullysMQTT[ip].name : client.id; - if (this.adapter.config.mqttConnErrorsAsInfo) { - this.adapter.log.info(`MQTT Client ${logMsgName} disconnected.`); - } else { - this.adapter.log.error(`[MQTT] Client ${logMsgName} disconnected.`); - } - this.setIsAlive(client.id, false, "client disconnected"); - }); - this.aedes.on("clientError", (client, e) => { - if (this.notAuthorizedClients.includes(client.id)) - return; - const ip = this.devices[client.id].ip; - const logMsgName = ip ? this.adapter.fullysMQTT[ip].name : client.id; - if (this.adapter.config.mqttConnErrorsAsInfo) { - this.adapter.log.info(`[MQTT] ${logMsgName}: Client error - ${e.message}`); - } else { - this.adapter.log.error(`[MQTT]\u{1F525} ${logMsgName}: Client error - ${e.message}`); - } - this.adapter.log.debug(`[MQTT]\u{1F525} ${logMsgName}: Client error - stack: ${e.stack}`); - this.setIsAlive(client.id, false, "client error"); - }); - this.aedes.on("connectionError", (client, e) => { - const ip = this.devices[client.id].ip; - const logMsgName = ip ? this.adapter.fullysMQTT[ip].name : client.id; - if (this.adapter.config.mqttConnErrorsAsInfo) { - this.adapter.log.info(`[MQTT] ${logMsgName}: Connection error - ${e.message}`); - } else { - this.adapter.log.error(`[MQTT]\u{1F525} ${logMsgName}: Connection error - ${e.message}`); + this.adapter.log.error(this.adapter.err2Str(e)); + return; } - this.adapter.log.debug(`[MQTT]\u{1F525} ${logMsgName}: Connection error - stack: ${e.stack}`); - this.setIsAlive(client.id, false, "connection error"); - }); - this.server.on("error", (e) => { - if (e instanceof Error && e.message.startsWith("listen EADDRINUSE")) { - this.adapter.log.debug(`[MQTT] Cannot start server - ${e.message}`); - this.adapter.log.error(`[MQTT]\u{1F525} Cannot start server - Port ${this.port} is already in use. Try a different port!`); + } + + setIsAlive(clientId, isAlive, msg) { + + if (isAlive) this.devices[clientId].lastTimeActive = Date.now(); + this.devices[clientId].isActive = isAlive; + + const ip = this.devices[clientId]?.ip; + if (ip) { + // Call Adapter function onMqttAliveChange() + this.adapter.onMqttAlive(ip, isAlive); + if (isAlive) { + this.scheduleCheckIfStillActive(clientId); // restart timer + } else { + // clear timer + // @ts-expect-error "Type 'null' is not assignable to type 'Timeout'.ts(2345)" - we check for not being null via "if" + if (this.devices[clientId].timeoutNoUpdate) this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate); + } } else { - this.adapter.log.error(`[MQTT]\u{1F525} Cannot start server - ${e.message}`); + this.adapter.log.debug(`[MQTT] isAlive changed to ${isAlive}, but IP of client ${clientId} is still unknown.`); } - this.terminate(); - }); - } catch (e) { - this.adapter.log.error(this.adapter.err2Str(e)); - return; } - } - - setIsAlive(clientId, isAlive, msg) { - - if (isAlive) this.devices[clientId].lastTimeActive = Date.now(); - this.devices[clientId].isActive = isAlive; - - const ip = this.devices[clientId]?.ip; - if (ip) { - // Call Adapter function onMqttAliveChange() - this.adapter.onMqttAlive(ip, isAlive, msg); - if (isAlive) { - this.scheduleCheckIfStillActive(clientId); // restart timer - } else { - // clear timer - // @ts-expect-error "Type 'null' is not assignable to type 'Timeout'.ts(2345)" - we check for not being null via "if" - if (this.devices[clientId].timeoutNoUpdate) this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate); - } - } else { - this.adapter.log.debug(`[MQTT] isAlive changed to ${isAlive}, but IP of client ${clientId} is still unknown.`); - } - } - - async scheduleCheckIfStillActive(clientId) { - try { - const ip = this.devices[clientId].ip; - const ipMsg = ip ? `${this.adapter.fullysMQTT[ip].name} (${ip})` : `${clientId} (IP unknown)`; - if (this.devices[clientId].timeoutNoUpdate) - this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate); - - if (!this.devices[clientId]) - this.devices[clientId] = {}; - - const interval = 70 * 1000; - - this.devices[clientId].timeoutNoUpdate = this.adapter.setTimeout(async () => { + async scheduleCheckIfStillActive(clientId) { try { - const lastTimeActive = this.devices[clientId].lastTimeActive; - if (!lastTimeActive) - return; - const diff = Date.now() - lastTimeActive; - if (diff > 70000) { - this.adapter.log.debug(`[MQTT] ${ipMsg} NOT ALIVE - last contact ${Math.round(diff / 1000)}s (${diff}ms) ago`); - this.setIsAlive(clientId, false, "client did not send message for more than 70 seconds"); - } else { - 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.`); - this.adapter.log.warn(`[MQTT] ${ipMsg} is alive - last contact ${Math.round(diff / 1000)}s (${diff}ms) ago`); - this.setIsAlive(clientId, true, `alive check is successful (last contact: ${Math.round(diff / 1000)}s ago)`); - } - this.scheduleCheckIfStillActive(clientId); + const ip = this.devices[clientId].ip; + const ipMsg = ip ? `${this.adapter.fullysMQTT[ip].name} (${ip})` : `${clientId} (IP unknown)`; + + if (this.devices[clientId].timeoutNoUpdate) + this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate); + + if (!this.devices[clientId]) + this.devices[clientId] = {}; + + const interval = 70 * 1000; + + this.devices[clientId].timeoutNoUpdate = this.adapter.setTimeout(async () => { + try { + const lastTimeActive = this.devices[clientId].lastTimeActive; + if (!lastTimeActive) + return; + const diff = Date.now() - lastTimeActive; + if (diff > 70000) { + this.adapter.log.debug(`[MQTT] ${ipMsg} NOT ALIVE - last contact ${Math.round(diff / 1000)}s (${diff}ms) ago`); + this.setIsAlive(clientId, false, 'client did not send message for more than 70 seconds'); + } else { + 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.`); + this.adapter.log.warn(`[MQTT] ${ipMsg} is alive - last contact ${Math.round(diff / 1000)}s (${diff}ms) ago`); + this.setIsAlive(clientId, true, `alive check is successful (last contact: ${Math.round(diff / 1000)}s ago)`); + } + this.scheduleCheckIfStillActive(clientId); + } catch (e) { + this.adapter.log.error(this.adapter.err2Str(e)); + return; + } + }, interval); } catch (e) { - this.adapter.log.error(this.adapter.err2Str(e)); - return; + this.adapter.log.error(this.adapter.err2Str(e)); + return; } - }, interval); - } catch (e) { - this.adapter.log.error(this.adapter.err2Str(e)); - return; - } - } - terminate() { - this.adapter.log.info(`[MQTT] Disconnect all clients and close server`); - for (const clientId in this.devices) { - if (this.devices[clientId].timeoutNoUpdate) - this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate); - this.setIsAlive(clientId, false, "MQTT server was terminated"); } - if (this.aedes) { - this.aedes.close(() => { - this.adapter.log.debug("[MQTT] aedes.close() succeeded"); - if (this.server) { - this.server.close(() => { - this.adapter.log.debug("[MQTT] server.close() succeeded"); - }); + terminate() { + this.adapter.log.info(`[MQTT] Disconnect all clients and close server`); + for (const clientId in this.devices) { + if (this.devices[clientId].timeoutNoUpdate) + this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate); + this.setIsAlive(clientId, false, 'MQTT server was terminated'); + } + if (this.aedes) { + this.aedes.close(() => { + this.adapter.log.debug('[MQTT] aedes.close() succeeded'); + if (this.server) { + this.server.close(() => { + this.adapter.log.debug('[MQTT] server.close() succeeded'); + }); + } + }); + } else if (this.server) { + this.server.close(() => { + this.adapter.log.debug('[MQTT] server.close() succeeded'); + }); } - }); - } else if (this.server) { - this.server.close(() => { - this.adapter.log.debug("[MQTT] server.close() succeeded"); - }); } - } } module.exports = MqttServer; diff --git a/lib/restApi.js b/lib/restApi.js index 22089e3..62fe094 100644 --- a/lib/restApi.js +++ b/lib/restApi.js @@ -3,165 +3,165 @@ const axios = require('axios'); class RestApiFully { - constructor(adapter) { - this.adapter = adapter; - } - - async sendCmd(device, cmd, val) { - try { - const cmds = { - textToSpeech: {urlParameter: "cmd=textToSpeech&text=", cleanSpaces: true, encode: true}, - loadURL: {urlParameter: "cmd=loadURL&url=", cleanSpaces: true, encode: true}, - startApplication: {urlParameter: "cmd=startApplication&package=", cleanSpaces: true}, - screenBrightness: {urlParameter: "cmd=setStringSetting&key=screenBrightness&value="}, - setAudioVolume: {urlParameter: "cmd=setAudioVolume&stream=3&level="} - }; - let finalUrlParam = ""; - if (cmd in cmds) { - if (cmds[cmd].cleanSpaces) { - val = val.toString().trim(); - val = val.replace(/\s+/g, " "); - } - if (cmds[cmd].encode) { - val = val.toString().trim(); - val = encodeURIComponent(val); + constructor(adapter) { + this.adapter = adapter; + } + + async sendCmd(device, cmd, val) { + try { + const cmds = { + textToSpeech: {urlParameter: 'cmd=textToSpeech&text=', cleanSpaces: true, encode: true}, + loadURL: {urlParameter: 'cmd=loadURL&url=', cleanSpaces: true, encode: true}, + startApplication: {urlParameter: 'cmd=startApplication&package=', cleanSpaces: true}, + screenBrightness: {urlParameter: 'cmd=setStringSetting&key=screenBrightness&value='}, + setAudioVolume: {urlParameter: 'cmd=setAudioVolume&stream=3&level='} + }; + let finalUrlParam = ''; + if (cmd in cmds) { + if (cmds[cmd].cleanSpaces) { + val = val.toString().trim(); + val = val.replace(/\s+/g, ' '); + } + if (cmds[cmd].encode) { + val = val.toString().trim(); + val = encodeURIComponent(val); + } + finalUrlParam = cmds[cmd].urlParameter + val; + } else { + finalUrlParam = 'cmd=' + cmd; + } + const result = await this.axiosSendCmd(device, cmd, finalUrlParam); + return result; + } catch (e) { + this.adapter.log.error(`[REST] ${device.name}: ${this.adapter.err2Str(e)}`); + return false; } - finalUrlParam = cmds[cmd].urlParameter + val; - } else { - finalUrlParam = "cmd=" + cmd; - } - const result = await this.axiosSendCmd(device, cmd, finalUrlParam); - return result; - } catch (e) { - this.adapter.log.error(`[REST] ${device.name}: ${this.adapter.err2Str(e)}`); - return false; } - } - async axiosSendCmd(device, cmd, urlParam) { + async axiosSendCmd(device, cmd, urlParam) { // Base URL - const url = `${device.restProtocol}://${device.ip}:${device.restPort}/?password=${this.encodePassword(device.restPassword)}&type=json&${urlParam}`; - - // Axios config - const config = { - method: 'get', - timeout: this.adapter.config.restTimeout, - }; - - try { - // Axios: Send command - const response = await axios.get(url, config); - - // Errors - if (response.status !== 200) { - this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: ${response.status} - ${response.statusText}`); - return false; - } - if (!('status' in response)) { - this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but it does not have key 'status'`); - return false; - } - if (!('data' in response)) { - this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but it does not have key 'data'`); - return false; - } - this.adapter.log.debug(`[REST] ${device.name}: Sending command ${cmd} response.data: ${JSON.stringify(response.data)}`); - - if (!('status' in response.data)) { - this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but response.data does not have key 'status'`); - return false; - } - switch (response.data.status) { - case 'OK': - this.adapter.log.debug(`[REST] ${device.name}: Sending command ${cmd} successful: - Status = "${response.data.status}", Message = "${response.data.statustext}"`); - return true; - case 'Error': - if (response.data.statustext === 'Please login') { - this.adapter.log.error(`[REST] ${device.name}: Error: Remote Admin Password seems to be incorrect. Sending command ${cmd} failed.`); - } else { - this.adapter.log.error(`[REST] ${device.name}: Error: Sending command ${cmd} failed, received status text: ${response.data.statustext}`); - } - return false; - default: - // Unexpected - this.adapter.log.error(`[REST] ${device.name}: Undefined response.data.status = "${response.data.status}" when sending command ${cmd}: ${response.status} - ${response.statusText}`); - return false; - } - } catch (err) { - this.errorFunction(err,device, cmd); - return false; + const url = `${device.restProtocol}://${device.ip}:${device.restPort}/?password=${this.encodePassword(device.restPassword)}&type=json&${urlParam}`; + + // Axios config + const config = { + method: 'get', + timeout: this.adapter.config.restTimeout, + }; + + try { + // Axios: Send command + const response = await axios.get(url, config); + + // Errors + if (response.status !== 200) { + this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: ${response.status} - ${response.statusText}`); + return false; + } + if (!('status' in response)) { + this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but it does not have key 'status'`); + return false; + } + if (!('data' in response)) { + this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but it does not have key 'data'`); + return false; + } + this.adapter.log.debug(`[REST] ${device.name}: Sending command ${cmd} response.data: ${JSON.stringify(response.data)}`); + + if (!('status' in response.data)) { + this.adapter.log.error(`[REST] ${device.name}: Sending command ${cmd} failed: Response received but response.data does not have key 'status'`); + return false; + } + switch (response.data.status) { + case 'OK': + this.adapter.log.debug(`[REST] ${device.name}: Sending command ${cmd} successful: - Status = "${response.data.status}", Message = "${response.data.statustext}"`); + return true; + case 'Error': + if (response.data.statustext === 'Please login') { + this.adapter.log.error(`[REST] ${device.name}: Error: Remote Admin Password seems to be incorrect. Sending command ${cmd} failed.`); + } else { + this.adapter.log.error(`[REST] ${device.name}: Error: Sending command ${cmd} failed, received status text: ${response.data.statustext}`); + } + return false; + default: + // Unexpected + this.adapter.log.error(`[REST] ${device.name}: Undefined response.data.status = "${response.data.status}" when sending command ${cmd}: ${response.status} - ${response.statusText}`); + return false; + } + } catch (err) { + this.errorFunction(err,device, cmd); + return false; + } + } + + errorFunction(err,device, cmd) { + const errTxt = `[REST] ${device.name}: Sending command ${cmd} failed`; + if (axios.isAxiosError(err)) { + if (!err?.response) { + this.adapter.log.warn(`${errTxt}: No response`); + } else if (err.response?.status === 400) { + this.adapter.log.error('${errTxt}: Login Failed - Error 400 - ' + err.response?.statusText); + } else if (err.response?.status) { + this.adapter.log.error(`${errTxt}: ${err.response.status} - ${err.response.statusText}`); + } else { + this.adapter.log.error(`${errTxt}: General Error`); + } + } else { + this.adapter.log.error(`${errTxt}: Error: ${this.adapter.err2Str(err)}`); + } } - } - - errorFunction(err,device, cmd) { - const errTxt = `[REST] ${device.name}: Sending command ${cmd} failed`; - if (axios.isAxiosError(err)) { - if (!err?.response) { - this.adapter.log.warn(`${errTxt}: No response`); - } else if (err.response?.status === 400) { - this.adapter.log.error('${errTxt}: Login Failed - Error 400 - ' + err.response?.statusText); - } else if (err.response?.status) { - this.adapter.log.error(`${errTxt}: ${err.response.status} - ${err.response.statusText}`); - } else { - this.adapter.log.error(`${errTxt}: General Error`); - } - } else { - this.adapter.log.error(`${errTxt}: Error: ${this.adapter.err2Str(err)}`); + + + encodePassword(pw) { + return encodeURIComponent(pw).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); } - } + async axiosGetDevicesInfo(device) { - encodePassword(pw) { - return encodeURIComponent(pw).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); - } + const url = `${device.restProtocol}://${device.ip}:${device.restPort}/?cmd=deviceInfo&type=json&password=${this.encodePassword(device.restPassword)}`; - async axiosGetDevicesInfo(device) { + // Axios config + const config = { + method: 'get', + timeout: this.adapter.config.restTimeout, + }; - const url = `${device.restProtocol}://${device.ip}:${device.restPort}/?cmd=deviceInfo&type=json&password=${this.encodePassword(device.restPassword)}`; + try { + // Axios: get infos + const response = await axios.get(url, config); - // Axios config - const config = { - method: 'get', - timeout: this.adapter.config.restTimeout, - }; + if (response.status == 200) { - try { - // Axios: get infos - const response = await axios.get(url, config); + const result = { + clientId: 9999, + ip: device.ip, + topic: 'fake', + infoObj: response.data + }; - if (response.status == 200) { + this.adapter.onMqttInfo(result); + await this.adapter.setStateAsync('info.connection', { val: true, ack: true }); - const result = { - clientId: 9999, - ip: device.ip, - topic: "fake", - infoObj: response.data - }; + } + } catch (err) { + this.adapter.aliveUpdate(device.id,false); + const cmd = 'get axiosGetDevicesInfo '; + this.errorFunction(err,device, cmd); + return false; + } + } - this.adapter.onMqttInfo(result); - await this.adapter.setStateAsync('info.connection', { val: true, ack: true }); + async startIntervall() { + for (const ip in this.adapter.fullysRESTApi) { + await this.axiosGetDevicesInfo(this.adapter.fullysRESTApi[ip]); + } - } - } catch (err) { - this.adapter.aliveUpdate(device.id,false); - const cmd = 'get axiosGetDevicesInfo '; - this.errorFunction(err,device, cmd); - return false; + if (!this.adapter._requestInterval) { + this.adapter.log.info(`\u{1F680} Start RESTApi request intervall`); + this.adapter._requestInterval = setInterval(async () => { + await this.startIntervall(); + }, this.adapter.config.restIntervall); + } } - } - - async startIntervall() { - for (const ip in this.adapter.fullysRESTApi) { - await this.axiosGetDevicesInfo(this.adapter.fullysRESTApi[ip]); - } - - if (!this.adapter._requestInterval) { - this.adapter.log.info(`\u{1F680} Start RESTApi request intervall`); - this.adapter._requestInterval = setInterval(async () => { - await this.startIntervall(); - }, this.adapter.config.restIntervall); - } - } } module.exports = RestApiFully; diff --git a/main.js b/main.js index 5d215f2..07ec730 100644 --- a/main.js +++ b/main.js @@ -1,714 +1,714 @@ const utils = require('@iobroker/adapter-core'); -const cmds = require("./lib/constants").cmds; -const cmdsSwitches = require("./lib/constants").cmdsSwitches; -const mqttEvents = require("./lib/constants").mqttEvents; -const _methods = require("./lib/methods"); -const MqttServer = require("./lib/mqtt-server"); -const RestApiFully = require("./lib/restApi"); +const cmds = require('./lib/constants').cmds; +const cmdsSwitches = require('./lib/constants').cmdsSwitches; +const mqttEvents = require('./lib/constants').mqttEvents; +const _methods = require('./lib/methods'); +const MqttServer = require('./lib/mqtt-server'); +const RestApiFully = require('./lib/restApi'); /** * ------------------------------------------------------------------- * ioBroker Fully Browser MQTT Adapter * ------------------------------------------------------------------- */ class fullybrowserControll extends utils.Adapter { - constructor(options = {}) { - super({ - ...options, - name: "fullybrowser" - }); - this.err2Str = _methods.err2Str.bind(this); - this.isEmpty = _methods.isEmpty.bind(this); - this.wait = _methods.wait.bind(this); - this.cleanDeviceName = _methods.cleanDeviceName.bind(this); - this.getConfigValuePerKey = _methods.getConfigValuePerKey.bind(this); - this.isIpAddressValid = _methods.isIpAddressValid.bind(this); - - this.restApi = new RestApiFully(this); - - this.fullysEnbl = {}; - this.fullysMQTT = {}; - this.fullysRESTApi = {}; - this.startMQTTServer = false; - - this.fullysDisbl = {}; - this.fullysAll = {}; - - this.onMqttAlive_EverBeenCalledBefore = false; - this._requestInterval = 0; - - this.on("ready", this.onReady.bind(this)); - this.on("stateChange", this.onStateChange.bind(this)); - this.on("unload", this.onUnload.bind(this)); - } - - async onReady() { - try { - this.setState("info.connection", {val: false, ack: true}); - - if (await this.initConfig()) { - this.log.debug(`Adapter settings successfully verified and initialized.`); - } else { - this.log.error(`Adapter settings initialization failed. ---> Please check your adapter instance settings!`); - return; - } - - // all enabled - for (const ip in this.fullysEnbl) { - const res = await this.createFullyDeviceObjects(this.fullysEnbl[ip]); - if (res) { - await this.subscribeStatesAsync(this.fullysEnbl[ip].id + ".Commands.*"); - } - if (this.fullysEnbl[ip].apiType == 'mqtt') { - this.startMQTTServer = true; - } - this.setState(this.fullysEnbl[ip].id + ".enabled", { - val: true, - ack: true - }); - this.setState(this.fullysEnbl[ip].id + ".apiType", { - val: this.fullysEnbl[ip].apiType, - ack: true + constructor(options = {}) { + super({ + ...options, + name: 'fullybrowser' }); - this.setState(this.fullysEnbl[ip].id + ".alive", { - val: false, - ack: true - }); - } - - // all disabled - for (const ip in this.fullysDisbl) { - if (await this.getObjectAsync(this.fullysAll[ip].id)) { - this.setState(this.fullysDisbl[ip].id + ".enabled", { - val: false, - ack: true - }); - this.setState(this.fullysDisbl[ip].id + ".apiType", { - val: this.fullysDisbl[ip].apiType, - ack: true - }); - this.setState(this.fullysDisbl[ip].id + ".alive", { - val: null, - ack: true - }); - } - } + this.err2Str = _methods.err2Str.bind(this); + this.isEmpty = _methods.isEmpty.bind(this); + this.wait = _methods.wait.bind(this); + this.cleanDeviceName = _methods.cleanDeviceName.bind(this); + this.getConfigValuePerKey = _methods.getConfigValuePerKey.bind(this); + this.isIpAddressValid = _methods.isIpAddressValid.bind(this); - if (this.startMQTTServer) { - this.mqtt_Server = new MqttServer(this); - this.mqtt_Server.start(); - } + this.restApi = new RestApiFully(this); - this.restApi.startIntervall(); + this.fullysEnbl = {}; + this.fullysMQTT = {}; + this.fullysRESTApi = {}; + this.startMQTTServer = false; - this.deleteRemovedDeviceObjects(); - } catch (e) { - this.log.error(this.err2Str(e)); - return; - } - } - - async onMqttAlive(ip, isAlive, msg) { - try { - const prevIsAlive = this.fullysEnbl[ip].isAlive; - this.fullysEnbl[ip].isAlive = isAlive; - - // Has this function ever been called before? If adapter is restarted, we ensure log, etc. - const calledBefore = this.onMqttAlive_EverBeenCalledBefore; // Keep old value - this.onMqttAlive_EverBeenCalledBefore = true; // Now it was called - - // if alive status changed - if ((!calledBefore && isAlive === true) || prevIsAlive !== isAlive) { - // Set Device isAlive Status - we could also use setStateChanged()... - this.setState(this.fullysEnbl[ip].id + '.alive', { val: isAlive, ack: true }); - } - - this.setStateChanged('info.connection', { val: true, ack: true }); - } catch (e) { - this.log.error(this.err2Str(e)); - return; + this.fullysDisbl = {}; + this.fullysAll = {}; + + this.onMqttAlive_EverBeenCalledBefore = false; + this._requestInterval = 0; + + this.on('ready', this.onReady.bind(this)); + this.on('stateChange', this.onStateChange.bind(this)); + this.on('unload', this.onUnload.bind(this)); } - } - - async onMqttInfo(obj) { - try { - const newInfoKeysAdded = []; - let valValue = 'value'; - let valType = ''; - - for (const key in obj.infoObj) { - const val = obj.infoObj[key]; - valType = typeof val; - valValue = 'value'; - - if (valType !== 'string' && valType !== 'boolean' && valType !== 'object' && valType !== 'number') { - this.log.warn(`[INFO] ${this.fullysEnbl[obj.ip].name}: Unknown type ${valType} of key '${key}' in info object`); - continue; - } - if (key == 'timestamp') { - valValue = 'value.time'; - } + async onReady() { + try { + this.setState('info.connection', {val: false, ack: true}); - if (!this.fullysEnbl[obj.ip].mqttInfoKeys.includes(key)) { - this.fullysEnbl[obj.ip].mqttInfoKeys.push(key); - - newInfoKeysAdded.push(key); - - await this.setObjectNotExistsAsync(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { - type: "state", - common: { - name: "Info: " + key, - type: valType, - role: valValue, - read: true, - write: false - }, - native: {} - }); - } - } + if (await this.initConfig()) { + this.log.debug(`Adapter settings successfully verified and initialized.`); + } else { + this.log.error(`Adapter settings initialization failed. ---> Please check your adapter instance settings!`); + return; + } - for (const key in obj.infoObj) { - const newVal = typeof obj.infoObj[key] === "object" ? JSON.stringify(obj.infoObj[key]) : obj.infoObj[key]; + // all enabled + for (const ip in this.fullysEnbl) { + const res = await this.createFullyDeviceObjects(this.fullysEnbl[ip]); + if (res) { + await this.subscribeStatesAsync(this.fullysEnbl[ip].id + '.Commands.*'); + } + if (this.fullysEnbl[ip].apiType == 'mqtt') { + this.startMQTTServer = true; + } + this.setState(this.fullysEnbl[ip].id + '.enabled', { + val: true, + ack: true + }); + this.setState(this.fullysEnbl[ip].id + '.apiType', { + val: this.fullysEnbl[ip].apiType, + ack: true + }); + this.setState(this.fullysEnbl[ip].id + '.alive', { + val: false, + ack: true + }); + } - if (this.config.mqttUpdateUnchangedObjects) { - this.setState(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { val: newVal, ack: true }); - } else { - this.setStateChanged(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { val: newVal, ack: true }); - } - } + // all disabled + for (const ip in this.fullysDisbl) { + if (await this.getObjectAsync(this.fullysAll[ip].id)) { + this.setState(this.fullysDisbl[ip].id + '.enabled', { + val: false, + ack: true + }); + this.setState(this.fullysDisbl[ip].id + '.apiType', { + val: this.fullysDisbl[ip].apiType, + ack: true + }); + this.setState(this.fullysDisbl[ip].id + '.alive', { + val: null, + ack: true + }); + } + } + + if (this.startMQTTServer) { + this.mqtt_Server = new MqttServer(this); + this.mqtt_Server.start(); + } - this.aliveUpdate(this.fullysEnbl[obj.ip].id,true); + this.restApi.startIntervall(); - } catch (e) { - this.log.error(this.err2Str(e)); - return; + this.deleteRemovedDeviceObjects(); + } catch (e) { + this.log.error(this.err2Str(e)); + return; + } } - } - - async aliveUpdate(dev,val) { - this.setState(dev + '.lastInfoUpdate', { val: Date.now(), ack: true }); - this.setState(dev + '.alive', { val: val, ack: true }); - } - - async onMqttEvent(obj) { - try { - this.log.debug(`[MQTT] \u{1F4E1} ${this.fullysMQTT[obj.ip].name} published event, topic: ${obj.topic}, cmd: ${obj.cmd}`); - const pthEvent = `${this.fullysMQTT[obj.ip].id}.Events.${obj.cmd}`; - if (!await this.getObjectAsync(pthEvent)) { - this.log.debug(`[MQTT] ${this.fullysMQTT[obj.ip].name}: Event ${obj.cmd} received but state ${pthEvent} does not exist, so we create it first`); - await this.setObjectNotExistsAsync(pthEvent, { - type: "state", - common: { - name: "Event: " + obj.cmd, - type: "boolean", - role: "switch", - read: true, - write: false - }, - native: {} - }); - } - this.setState(pthEvent, { - val: true, - ack: true - }); - const pthCmd = this.fullysMQTT[obj.ip].id + ".Commands"; - const idx = this.getIndexFromConf(cmdsSwitches, ["mqttOn", "mqttOff"], obj.cmd); - if (idx !== -1) { - const conf = cmdsSwitches[idx]; - const onOrOffCmd = obj.cmd === conf.mqttOn ? true : false; - await this.setStateAsync(`${pthCmd}.${conf.id}`, { - val: onOrOffCmd, - ack: true - }); - await this.setStateAsync(`${pthCmd}.${conf.cmdOn}`, { - val: onOrOffCmd, - ack: true - }); - await this.setStateAsync(`${pthCmd}.${conf.cmdOff}`, { - val: !onOrOffCmd, - ack: true - }); - } else { - const idx2 = this.getIndexFromConf(cmds, ["id"], obj.cmd); - if (idx2 !== -1 && cmds[idx2].type === "boolean") { - await this.setStateAsync(`${pthCmd}.${obj.cmd}`, { - val: true, - ack: true - }); - } else { - this.log.silly(`[MQTT] ${this.fullysMQTT[obj.ip].name}: Event cmd ${obj.cmd} - no REST API command is existing, so skip confirmation with with ack:true`); + + async onMqttAlive(ip, isAlive) { + try { + const prevIsAlive = this.fullysEnbl[ip].isAlive; + this.fullysEnbl[ip].isAlive = isAlive; + + // Has this function ever been called before? If adapter is restarted, we ensure log, etc. + const calledBefore = this.onMqttAlive_EverBeenCalledBefore; // Keep old value + this.onMqttAlive_EverBeenCalledBefore = true; // Now it was called + + // if alive status changed + if ((!calledBefore && isAlive === true) || prevIsAlive !== isAlive) { + // Set Device isAlive Status - we could also use setStateChanged()... + this.setState(this.fullysEnbl[ip].id + '.alive', { val: isAlive, ack: true }); + } + + this.setStateChanged('info.connection', { val: true, ack: true }); + } catch (e) { + this.log.error(this.err2Str(e)); + return; } - } - } catch (e) { - this.log.error(this.err2Str(e)); - return; } - } - - async onStateChange(stateId, stateObj) { - try { - if (!stateObj) - return; - if (stateObj.ack) - return; - const idSplit = stateId.split("."); - const deviceId = idSplit[2]; - const channel = idSplit[3]; - const cmd = idSplit[4]; - const pth = deviceId + "." + channel; - - if (channel === "Commands") { - this.log.debug(`state ${stateId} changed: ${stateObj.val} (ack = ${stateObj.ack})`); - const fully = this.getFullyByKey("id", deviceId); - - if (!fully) - throw `Fully object for deviceId '${deviceId}' not found!`; - - let cmdToSend = cmd; - let switchConf = void 0; - const idxSw = this.getIndexFromConf(cmdsSwitches, ["id"], cmd); - if (idxSw !== -1) { - switchConf = cmdsSwitches[idxSw]; - cmdToSend = stateObj.val ? switchConf.cmdOn : switchConf.cmdOff; - } else { - if (!stateObj.val) + + async onMqttInfo(obj) { + try { + const newInfoKeysAdded = []; + let valValue = 'value'; + let valType = ''; + + for (const key in obj.infoObj) { + const val = obj.infoObj[key]; + valType = typeof val; + valValue = 'value'; + + if (valType !== 'string' && valType !== 'boolean' && valType !== 'object' && valType !== 'number') { + this.log.warn(`[INFO] ${this.fullysEnbl[obj.ip].name}: Unknown type ${valType} of key '${key}' in info object`); + continue; + } + + if (key == 'timestamp') { + valValue = 'value.time'; + } + + if (!this.fullysEnbl[obj.ip].mqttInfoKeys.includes(key)) { + this.fullysEnbl[obj.ip].mqttInfoKeys.push(key); + + newInfoKeysAdded.push(key); + + await this.setObjectNotExistsAsync(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { + type: 'state', + common: { + name: 'Info: ' + key, + type: valType, + role: valValue, + read: true, + write: false + }, + native: {} + }); + } + } + + for (const key in obj.infoObj) { + const newVal = typeof obj.infoObj[key] === 'object' ? JSON.stringify(obj.infoObj[key]) : obj.infoObj[key]; + + if (this.config.mqttUpdateUnchangedObjects) { + this.setState(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { val: newVal, ack: true }); + } else { + this.setStateChanged(`${this.fullysEnbl[obj.ip].id}.Info.${key}`, { val: newVal, ack: true }); + } + } + + this.aliveUpdate(this.fullysEnbl[obj.ip].id,true); + + } catch (e) { + this.log.error(this.err2Str(e)); return; } + } - if (!cmdToSend) - throw `onStateChange() - ${stateId}: fullyCmd could not be determined!`; - - const sendCommand = await this.restApi.sendCmd(fully, cmdToSend, stateObj.val); - - if (sendCommand) { - if (this.config.restCommandLogAsDebug) { - this.log.debug(`\u{1F5F8} ${fully.name}: Command ${cmd} successfully set to ${stateObj.val}`); - } else { - this.log.info(`\u{1F5F8} ${fully.name}: Command ${cmd} successfully set to ${stateObj.val}`); - } - - if (switchConf !== void 0) { - const onOrOffCmdVal = cmd === switchConf.cmdOn ? true : false; - await this.setStateAsync(`${pth}.${switchConf.id}`, { - val: onOrOffCmdVal, - ack: true - }); - await this.setStateAsync(`${pth}.${switchConf.cmdOn}`, { - val: onOrOffCmdVal, - ack: true - }); - await this.setStateAsync(`${pth}.${switchConf.cmdOff}`, { - val: !onOrOffCmdVal, - ack: true + async aliveUpdate(dev,val) { + this.setState(dev + '.lastInfoUpdate', { val: Date.now(), ack: true }); + this.setState(dev + '.alive', { val: val, ack: true }); + } + + async onMqttEvent(obj) { + try { + this.log.debug(`[MQTT] \u{1F4E1} ${this.fullysMQTT[obj.ip].name} published event, topic: ${obj.topic}, cmd: ${obj.cmd}`); + const pthEvent = `${this.fullysMQTT[obj.ip].id}.Events.${obj.cmd}`; + if (!await this.getObjectAsync(pthEvent)) { + this.log.debug(`[MQTT] ${this.fullysMQTT[obj.ip].name}: Event ${obj.cmd} received but state ${pthEvent} does not exist, so we create it first`); + await this.setObjectNotExistsAsync(pthEvent, { + type: 'state', + common: { + name: 'Event: ' + obj.cmd, + type: 'boolean', + role: 'switch', + read: true, + write: false + }, + native: {} + }); + } + this.setState(pthEvent, { + val: true, + ack: true }); - } else { - if (typeof stateObj.val === "boolean") { - const idx = this.getIndexFromConf(cmds, ["id"], cmd); - if (idx !== -1) { - if (cmds[idx].type === "boolean") { - await this.setStateAsync(stateId, { - val: true, + const pthCmd = this.fullysMQTT[obj.ip].id + '.Commands'; + const idx = this.getIndexFromConf(cmdsSwitches, ['mqttOn', 'mqttOff'], obj.cmd); + if (idx !== -1) { + const conf = cmdsSwitches[idx]; + const onOrOffCmd = obj.cmd === conf.mqttOn ? true : false; + await this.setStateAsync(`${pthCmd}.${conf.id}`, { + val: onOrOffCmd, ack: true - }); - } else { - this.log.warn(`${fully.name}: ${stateId} - val: ${stateObj.val} is boolean, but cmd ${cmd} is not defined in CONF`); - await this.setStateAsync(stateId, { - val: stateObj.val, + }); + await this.setStateAsync(`${pthCmd}.${conf.cmdOn}`, { + val: onOrOffCmd, ack: true - }); - } - } else { - this.log.warn(`${fully.name}: ${stateId} - val: ${stateObj.val}, cmd ${cmd} is not defined in CONF`); - } + }); + await this.setStateAsync(`${pthCmd}.${conf.cmdOff}`, { + val: !onOrOffCmd, + ack: true + }); } else { - await this.setStateAsync(stateId, { - val: stateObj.val, - ack: true - }); + const idx2 = this.getIndexFromConf(cmds, ['id'], obj.cmd); + if (idx2 !== -1 && cmds[idx2].type === 'boolean') { + await this.setStateAsync(`${pthCmd}.${obj.cmd}`, { + val: true, + ack: true + }); + } else { + this.log.silly(`[MQTT] ${this.fullysMQTT[obj.ip].name}: Event cmd ${obj.cmd} - no REST API command is existing, so skip confirmation with with ack:true`); + } } - } - } else { - this.log.debug(`${fully.name}: restApiSendCmd() was not successful (${stateId})`); + } catch (e) { + this.log.error(this.err2Str(e)); + return; } - } - } catch (e) { - this.log.error(this.err2Str(e)); - return; } - } - - getFullyByKey(keyId, value) { - for (const ip in this.fullysEnbl) { - if (keyId in this.fullysEnbl[ip]) { - const lpKeyId = keyId; - const lpVal = this.fullysEnbl[ip][lpKeyId]; - if (lpVal === value) { - return this.fullysEnbl[ip]; + + async onStateChange(stateId, stateObj) { + try { + if (!stateObj) + return; + if (stateObj.ack) + return; + const idSplit = stateId.split('.'); + const deviceId = idSplit[2]; + const channel = idSplit[3]; + const cmd = idSplit[4]; + const pth = deviceId + '.' + channel; + + if (channel === 'Commands') { + this.log.debug(`state ${stateId} changed: ${stateObj.val} (ack = ${stateObj.ack})`); + const fully = this.getFullyByKey('id', deviceId); + + if (!fully) + throw `Fully object for deviceId '${deviceId}' not found!`; + + let cmdToSend = cmd; + let switchConf = void 0; + const idxSw = this.getIndexFromConf(cmdsSwitches, ['id'], cmd); + if (idxSw !== -1) { + switchConf = cmdsSwitches[idxSw]; + cmdToSend = stateObj.val ? switchConf.cmdOn : switchConf.cmdOff; + } else { + if (!stateObj.val) + return; + } + + if (!cmdToSend) + throw `onStateChange() - ${stateId}: fullyCmd could not be determined!`; + + const sendCommand = await this.restApi.sendCmd(fully, cmdToSend, stateObj.val); + + if (sendCommand) { + if (this.config.restCommandLogAsDebug) { + this.log.debug(`\u{1F5F8} ${fully.name}: Command ${cmd} successfully set to ${stateObj.val}`); + } else { + this.log.info(`\u{1F5F8} ${fully.name}: Command ${cmd} successfully set to ${stateObj.val}`); + } + + if (switchConf !== void 0) { + const onOrOffCmdVal = cmd === switchConf.cmdOn ? true : false; + await this.setStateAsync(`${pth}.${switchConf.id}`, { + val: onOrOffCmdVal, + ack: true + }); + await this.setStateAsync(`${pth}.${switchConf.cmdOn}`, { + val: onOrOffCmdVal, + ack: true + }); + await this.setStateAsync(`${pth}.${switchConf.cmdOff}`, { + val: !onOrOffCmdVal, + ack: true + }); + } else { + if (typeof stateObj.val === 'boolean') { + const idx = this.getIndexFromConf(cmds, ['id'], cmd); + if (idx !== -1) { + if (cmds[idx].type === 'boolean') { + await this.setStateAsync(stateId, { + val: true, + ack: true + }); + } else { + this.log.warn(`${fully.name}: ${stateId} - val: ${stateObj.val} is boolean, but cmd ${cmd} is not defined in CONF`); + await this.setStateAsync(stateId, { + val: stateObj.val, + ack: true + }); + } + } else { + this.log.warn(`${fully.name}: ${stateId} - val: ${stateObj.val}, cmd ${cmd} is not defined in CONF`); + } + } else { + await this.setStateAsync(stateId, { + val: stateObj.val, + ack: true + }); + } + } + } else { + this.log.debug(`${fully.name}: restApiSendCmd() was not successful (${stateId})`); + } + } + } catch (e) { + this.log.error(this.err2Str(e)); + return; } - } } - return false; - } - - getIndexFromConf(config, keys, cmd) { - try { - let index = -1; - for (const key of keys) { - index = config.findIndex((x) => x[key] === cmd); - if (index !== -1) - break; - } - return index; - } catch (e) { - this.log.error(this.err2Str(e)); - return -1; - } - } - - async onUnload(callback) { - try { - if (this._requestInterval) clearInterval(this._requestInterval); - - if (this.fullysAll) { - for (const ip in this.fullysAll) { - if (await this.getObjectAsync(this.fullysAll[ip].id)) { - this.setState(this.fullysAll[ip].id + ".alive", { - val: null, - ack: true - }); - } - } - } - if (this.mqtt_Server) { - for (const clientId in this.mqtt_Server.devices) { - if (this.mqtt_Server.devices[clientId].timeoutNoUpdate) - this.clearTimeout(this.mqtt_Server.devices[clientId].timeoutNoUpdate); + + getFullyByKey(keyId, value) { + for (const ip in this.fullysEnbl) { + if (keyId in this.fullysEnbl[ip]) { + const lpKeyId = keyId; + const lpVal = this.fullysEnbl[ip][lpKeyId]; + if (lpVal === value) { + return this.fullysEnbl[ip]; + } + } } - } - if (this.mqtt_Server) { - this.mqtt_Server.terminate(); - } - callback(); - } catch (e) { - callback(); - } - } - - async initConfig() { - try { - if (this.isEmpty(this.config.mqttPort) || this.config.mqttPort < 1 || this.config.mqttPort > 65535) { - this.log.warn(`Adapter instance settings: MQTT Port ${this.config.mqttPort} is not allowed, set to default of 1886`); - this.config.mqttPort = 1886; - } - if (this.isEmpty(this.config.mqttPublishedInfoDelay) || this.config.mqttPublishedInfoDelay < 2 || this.config.mqttPublishedInfoDelay > 120) { - this.log.warn(`Adapter instance settings: MQTT Publish Info Delay of ${this.config.mqttPublishedInfoDelay}s is not allowed, set to default of 30s`); - this.config.mqttPublishedInfoDelay = 30; - } - if (this.isEmpty(this.config.restTimeout) || this.config.restTimeout < 500 || this.config.restTimeout > 15000) { - this.log.warn(`Adapter instance settings: REST API timeout of ${this.config.restTimeout} ms is not allowed, set to default of 6000ms`); - this.config.restTimeout = 6000; - } - if (this.isEmpty(this.config.restIntervall) || this.config.restIntervall < 10000 || this.config.restIntervall > 99999999) { - this.log.warn(`Adapter instance settings: REST API timeout of ${this.config.restIntervall} ms is not allowed, set to default of 10000ms`); - this.config.restIntervall = 10000; - } - if (this.isEmpty(this.config.tableDevices)) { - this.log.error(`No Fully devices defined in adapter instance settings!`); return false; - } - const deviceIds = []; - const deviceIPs = []; - - for (let i = 0; i < this.config.tableDevices.length; i++) { - const lpDevice = this.config.tableDevices[i]; - const finalDevice = { - name: "", - id: "", - ip: "", - enabled: false, - mqttInfoObjectsCreated: false, - mqttInfoKeys: [], - restProtocol: "http", - restPort: 0, - restPassword: "", - lastSeen: 0, - isAlive: false, - apiType: "" - }; - if (!this.isIpAddressValid(lpDevice.ip)) { - this.log.error(`${finalDevice.name}: Provided IP address "${lpDevice.ip}" is not valid!`); - return false; + } + + getIndexFromConf(config, keys, cmd) { + try { + let index = -1; + for (const key of keys) { + index = config.findIndex((x) => x[key] === cmd); + if (index !== -1) + break; + } + return index; + } catch (e) { + this.log.error(this.err2Str(e)); + return -1; } - if (this.isEmpty(lpDevice.name)) { - this.log.error(`Provided device name "${lpDevice.name}" is empty!`); - return false; + } + + async onUnload(callback) { + try { + if (this._requestInterval) clearInterval(this._requestInterval); + + if (this.fullysAll) { + for (const ip in this.fullysAll) { + if (await this.getObjectAsync(this.fullysAll[ip].id)) { + this.setState(this.fullysAll[ip].id + '.alive', { + val: null, + ack: true + }); + } + } + } + if (this.mqtt_Server) { + for (const clientId in this.mqtt_Server.devices) { + if (this.mqtt_Server.devices[clientId].timeoutNoUpdate) + this.clearTimeout(this.mqtt_Server.devices[clientId].timeoutNoUpdate); + } + } + if (this.mqtt_Server) { + this.mqtt_Server.terminate(); + } + callback(); + } catch (e) { + callback(); } + } - finalDevice.name = lpDevice.ip.trim(); - finalDevice.id = this.cleanDeviceName(lpDevice.name); + async initConfig() { + try { + if (this.isEmpty(this.config.mqttPort) || this.config.mqttPort < 1 || this.config.mqttPort > 65535) { + this.log.warn(`Adapter instance settings: MQTT Port ${this.config.mqttPort} is not allowed, set to default of 1886`); + this.config.mqttPort = 1886; + } + if (this.isEmpty(this.config.mqttPublishedInfoDelay) || this.config.mqttPublishedInfoDelay < 2 || this.config.mqttPublishedInfoDelay > 120) { + this.log.warn(`Adapter instance settings: MQTT Publish Info Delay of ${this.config.mqttPublishedInfoDelay}s is not allowed, set to default of 30s`); + this.config.mqttPublishedInfoDelay = 30; + } + if (this.isEmpty(this.config.restTimeout) || this.config.restTimeout < 500 || this.config.restTimeout > 15000) { + this.log.warn(`Adapter instance settings: REST API timeout of ${this.config.restTimeout} ms is not allowed, set to default of 6000ms`); + this.config.restTimeout = 6000; + } + if (this.isEmpty(this.config.restIntervall) || this.config.restIntervall < 10000 || this.config.restIntervall > 99999999) { + this.log.warn(`Adapter instance settings: REST API timeout of ${this.config.restIntervall} ms is not allowed, set to default of 10000ms`); + this.config.restIntervall = 10000; + } + if (this.isEmpty(this.config.tableDevices)) { + this.log.error(`No Fully devices defined in adapter instance settings!`); + return false; + } + const deviceIds = []; + const deviceIPs = []; + + for (let i = 0; i < this.config.tableDevices.length; i++) { + const lpDevice = this.config.tableDevices[i]; + const finalDevice = { + name: '', + id: '', + ip: '', + enabled: false, + mqttInfoObjectsCreated: false, + mqttInfoKeys: [], + restProtocol: 'http', + restPort: 0, + restPassword: '', + lastSeen: 0, + isAlive: false, + apiType: '' + }; + if (!this.isIpAddressValid(lpDevice.ip)) { + this.log.error(`${finalDevice.name}: Provided IP address "${lpDevice.ip}" is not valid!`); + return false; + } + if (this.isEmpty(lpDevice.name)) { + this.log.error(`Provided device name "${lpDevice.name}" is empty!`); + return false; + } - if (finalDevice.id.length < 1) { - this.log.error(`Provided device name "${lpDevice.name}" is too short and/or has invalid characters!`); - return false; - } + finalDevice.name = lpDevice.ip.trim(); + finalDevice.id = this.cleanDeviceName(lpDevice.name); - if (lpDevice.apiType !== "mqtt" && lpDevice.apiType !== "restapi") { - this.log.warn(`${finalDevice.name}: apiType is empty, set to mqtt as default.`); - finalDevice.restProtocol = "mqtt"; - } else { - finalDevice.apiType = lpDevice.apiType; - } + if (finalDevice.id.length < 1) { + this.log.error(`Provided device name "${lpDevice.name}" is too short and/or has invalid characters!`); + return false; + } - if (deviceIds.includes(finalDevice.id)) { - this.log.error(`Device "${finalDevice.name}" -> id:"${finalDevice.id}" is used for more than once device.`); - return false; - } else { - deviceIds.push(finalDevice.id); - } + if (lpDevice.apiType !== 'mqtt' && lpDevice.apiType !== 'restapi') { + this.log.warn(`${finalDevice.name}: apiType is empty, set to mqtt as default.`); + finalDevice.restProtocol = 'mqtt'; + } else { + finalDevice.apiType = lpDevice.apiType; + } - if (lpDevice.restProtocol !== "http" && lpDevice.restProtocol !== "https") { - this.log.warn(`${finalDevice.name}: REST API Protocol is empty, set to http as default.`); - finalDevice.restProtocol = "http"; - } else { - finalDevice.restProtocol = lpDevice.restProtocol; - } - if (!this.isIpAddressValid(lpDevice.ip)) { - this.log.error(`${finalDevice.name}: Provided IP address "${lpDevice.ip}" is not valid!`); - return false; - } - if (deviceIPs.includes(lpDevice.ip)) { - this.log.error(`Device "${finalDevice.name}" -> IP:"${lpDevice.ip}" is used for more than once device.`); - return false; - } else { - deviceIPs.push(lpDevice.ip); - finalDevice.ip = lpDevice.ip; - } - if (isNaN(lpDevice.restPort) || lpDevice.restPort < 0 || lpDevice.restPort > 65535) { - this.log.error(`Adapter config Fully port number ${lpDevice.restPort} is not valid, should be >= 0 and < 65536.`); - return false; - } else { - finalDevice.restPort = Math.round(lpDevice.restPort); - } - if ((0, _methods.isEmpty)(lpDevice.restPassword)) { - this.log.error(`Remote Admin (REST API) Password must not be empty!`); - return false; - } else { - finalDevice.restPassword = lpDevice.restPassword; - } + if (deviceIds.includes(finalDevice.id)) { + this.log.error(`Device "${finalDevice.name}" -> id:"${finalDevice.id}" is used for more than once device.`); + return false; + } else { + deviceIds.push(finalDevice.id); + } - finalDevice.enabled = lpDevice.enabled ? true : false; + if (lpDevice.restProtocol !== 'http' && lpDevice.restProtocol !== 'https') { + this.log.warn(`${finalDevice.name}: REST API Protocol is empty, set to http as default.`); + finalDevice.restProtocol = 'http'; + } else { + finalDevice.restProtocol = lpDevice.restProtocol; + } + if (!this.isIpAddressValid(lpDevice.ip)) { + this.log.error(`${finalDevice.name}: Provided IP address "${lpDevice.ip}" is not valid!`); + return false; + } + if (deviceIPs.includes(lpDevice.ip)) { + this.log.error(`Device "${finalDevice.name}" -> IP:"${lpDevice.ip}" is used for more than once device.`); + return false; + } else { + deviceIPs.push(lpDevice.ip); + finalDevice.ip = lpDevice.ip; + } + if (isNaN(lpDevice.restPort) || lpDevice.restPort < 0 || lpDevice.restPort > 65535) { + this.log.error(`Adapter config Fully port number ${lpDevice.restPort} is not valid, should be >= 0 and < 65536.`); + return false; + } else { + finalDevice.restPort = Math.round(lpDevice.restPort); + } + if ((0, _methods.isEmpty)(lpDevice.restPassword)) { + this.log.error(`Remote Admin (REST API) Password must not be empty!`); + return false; + } else { + finalDevice.restPassword = lpDevice.restPassword; + } - const logConfig = { - ...finalDevice - }; + finalDevice.enabled = lpDevice.enabled ? true : false; - logConfig.restPassword = "(hidden)"; - this.log.debug(`Final Config: ${JSON.stringify(logConfig)}`); - this.fullysAll[finalDevice.ip] = finalDevice; + const logConfig = { + ...finalDevice + }; - if (lpDevice.enabled) { - this.fullysEnbl[finalDevice.ip] = finalDevice; + logConfig.restPassword = '(hidden)'; + this.log.debug(`Final Config: ${JSON.stringify(logConfig)}`); + this.fullysAll[finalDevice.ip] = finalDevice; - if (finalDevice.apiType == 'mqtt') { - this.fullysMQTT[finalDevice.ip] = finalDevice; - } else { - this.fullysRESTApi[finalDevice.ip] = finalDevice; - } + if (lpDevice.enabled) { + this.fullysEnbl[finalDevice.ip] = finalDevice; - this.log.info(`\u{1F5F8} ${finalDevice.name} (${finalDevice.ip}): Config successfully verified.`); - } else { - this.fullysDisbl[finalDevice.ip] = finalDevice; - this.log.info(`${finalDevice.name} (${finalDevice.ip}) is not enabled in settings, so it will not be used by adapter.`); - } + if (finalDevice.apiType == 'mqtt') { + this.fullysMQTT[finalDevice.ip] = finalDevice; + } else { + this.fullysRESTApi[finalDevice.ip] = finalDevice; + } + this.log.info(`\u{1F5F8} ${finalDevice.name} (${finalDevice.ip}): Config successfully verified.`); + } else { + this.fullysDisbl[finalDevice.ip] = finalDevice; + this.log.info(`${finalDevice.name} (${finalDevice.ip}) is not enabled in settings, so it will not be used by adapter.`); + } - } - if (Object.keys(this.fullysEnbl).length === 0) { - this.log.error(`No active devices with correct configuration found.`); - return false; - } - return true; - } catch (e) { - this.log.error(this.err2Str(e)); - return false; + + } + if (Object.keys(this.fullysEnbl).length === 0) { + this.log.error(`No active devices with correct configuration found.`); + return false; + } + return true; + } catch (e) { + this.log.error(this.err2Str(e)); + return false; + } } - } - - async createFullyDeviceObjects(device) { - try { - await this.setObjectNotExistsAsync(device.id, { - type: "device", - common: { - name: device.name, - statusStates: { - onlineId: `${this.namespace}.${device.id}.alive` - } - }, - native: {} - }); - await this.setObjectNotExistsAsync(device.id + ".Info", { - type: "channel", - common: { - name: "Device Information" - }, - native: {} - }); - await this.setObjectNotExistsAsync(device.id + ".alive", { - type: "state", - common: { - name: "Is Fully alive?", - desc: "If Fully Browser is alive or not", - type: "boolean", - role: "indicator.reachable", - read: true, - write: false - }, - native: {} - }); - 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: {} - }); - 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: {} - }); - await this.setObjectNotExistsAsync(device.id + ".apiType", { - type: "state", - common: { - name: "Which ApiType is selected for request?", - type: "string", - role: "value", - read: true, - write: false - }, - native: {} - }); - await this.setObjectNotExistsAsync(device.id + ".Commands", { - type: "channel", - common: { - name: "Commands" - }, - native: {} - }); - - const allCommands = cmds.concat(cmdsSwitches); - - for (const cmdObj of allCommands) { - let lpRole = ""; - if (cmdObj.type === "boolean") - lpRole = "button"; - if (cmdObj.type === "string") - lpRole = "text"; - if (cmdObj.type === "number") - lpRole = "value"; - if (cmdObj.cmdOn && cmdObj.cmdOff) - lpRole = "switch"; - 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: {} - }); - } - - if (device.apiType == 'mqtt') { - await this.setObjectNotExistsAsync(device.id + ".Events", { - type: "channel", - common: { - name: "MQTT Events" - }, - native: {} - }); - if (this.config.mqttCreateDefaultEventObjects) { - for (const event of mqttEvents) { - await this.setObjectNotExistsAsync(device.id + ".Events." + event, { - type: "state", - common: { - name: "Event: " + event, - type: "boolean", - role: "switch", - read: true, - write: false - }, - native: {} + async createFullyDeviceObjects(device) { + try { + await this.setObjectNotExistsAsync(device.id, { + type: 'device', + common: { + name: device.name, + statusStates: { + onlineId: `${this.namespace}.${device.id}.alive` + } + }, + native: {} + }); + await this.setObjectNotExistsAsync(device.id + '.Info', { + type: 'channel', + common: { + name: 'Device Information' + }, + native: {} + }); + await this.setObjectNotExistsAsync(device.id + '.alive', { + type: 'state', + common: { + name: 'Is Fully alive?', + desc: 'If Fully Browser is alive or not', + type: 'boolean', + role: 'indicator.reachable', + read: true, + write: false + }, + native: {} + }); + 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: {} + }); + 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: {} + }); + await this.setObjectNotExistsAsync(device.id + '.apiType', { + type: 'state', + common: { + name: 'Which ApiType is selected for request?', + type: 'string', + role: 'value', + read: true, + write: false + }, + native: {} + }); + await this.setObjectNotExistsAsync(device.id + '.Commands', { + type: 'channel', + common: { + name: 'Commands' + }, + native: {} }); - } - } - } - return true; - } catch (e) { - this.log.error(this.err2Str(e)); - return false; - } - } - - async deleteRemovedDeviceObjects() { - try { - const adapterObjectsIds = Object.keys(await this.getAdapterObjectsAsync()); - const allObjectDeviceIds = []; - for (const objectId of adapterObjectsIds) { - const deviceId = objectId.split(".")[2]; - if (["info"].includes(deviceId)) { - this.log.silly(`Cleanup: Ignore non device related state ${objectId}.`); - } else { - if (!allObjectDeviceIds.includes(deviceId)) - allObjectDeviceIds.push(deviceId); + const allCommands = cmds.concat(cmdsSwitches); + + for (const cmdObj of allCommands) { + let lpRole = ''; + if (cmdObj.type === 'boolean') + lpRole = 'button'; + if (cmdObj.type === 'string') + lpRole = 'text'; + if (cmdObj.type === 'number') + lpRole = 'value'; + if (cmdObj.cmdOn && cmdObj.cmdOff) + lpRole = 'switch'; + 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: {} + }); + } + + if (device.apiType == 'mqtt') { + await this.setObjectNotExistsAsync(device.id + '.Events', { + type: 'channel', + common: { + name: 'MQTT Events' + }, + native: {} + }); + + if (this.config.mqttCreateDefaultEventObjects) { + for (const event of mqttEvents) { + await this.setObjectNotExistsAsync(device.id + '.Events.' + event, { + type: 'state', + common: { + name: 'Event: ' + event, + type: 'boolean', + role: 'switch', + read: true, + write: false + }, + native: {} + }); + } + } + } + + return true; + } catch (e) { + this.log.error(this.err2Str(e)); + return false; } - } - const allConfigDeviceIds = []; - for (const ip in this.fullysAll) { - allConfigDeviceIds.push(this.fullysAll[ip].id); - } - for (const id of allObjectDeviceIds) { - if (!allConfigDeviceIds.includes(id)) { - await this.delObjectAsync(id, { - recursive: true - }); - this.log.info(`Cleanup: Deleted no longer defined device objects of '${id}'.`); + } + + async deleteRemovedDeviceObjects() { + try { + const adapterObjectsIds = Object.keys(await this.getAdapterObjectsAsync()); + const allObjectDeviceIds = []; + for (const objectId of adapterObjectsIds) { + const deviceId = objectId.split('.')[2]; + if (['info'].includes(deviceId)) { + this.log.silly(`Cleanup: Ignore non device related state ${objectId}.`); + } else { + if (!allObjectDeviceIds.includes(deviceId)) + allObjectDeviceIds.push(deviceId); + } + } + const allConfigDeviceIds = []; + for (const ip in this.fullysAll) { + allConfigDeviceIds.push(this.fullysAll[ip].id); + } + for (const id of allObjectDeviceIds) { + if (!allConfigDeviceIds.includes(id)) { + await this.delObjectAsync(id, { + recursive: true + }); + this.log.info(`Cleanup: Deleted no longer defined device objects of '${id}'.`); + } + } + } catch (e) { + this.log.error(this.err2Str(e)); + return; } - } - } catch (e) { - this.log.error(this.err2Str(e)); - return; } - } } // @ts-ignore parent is a valid property on module if (module.parent) { - // Export the constructor in compact mode - /** + // Export the constructor in compact mode + /** * @param {Partial} [options={}] */ - module.exports = (options) => new fullybrowserControll(options); + module.exports = (options) => new fullybrowserControll(options); } else { - // otherwise start the instance directly - new fullybrowserControll(); + // otherwise start the instance directly + new fullybrowserControll(); }