diff --git a/admin/jsonConfig.json b/admin/jsonConfig.json index dcf0550..ab8b2af 100644 --- a/admin/jsonConfig.json +++ b/admin/jsonConfig.json @@ -78,6 +78,25 @@ "title": "", "default": true }, + { + "type": "select", + "title": "ApiType", + "attr": "apiType", + "default": "mqtt", + "width": "6%", + "validator": "data.apiType.length > 3", + "validatorNoSaveOnError": true, + "options": [ + { + "label": "MQTT", + "value": "mqtt" + }, + { + "label": "RestApi", + "value": "restapi" + } + ] + }, { "type": "text", "title": "Device Name", @@ -243,13 +262,24 @@ "md": 6, "lg": 3 }, + "restIntervall": { + "type": "number", + "min": 10000, + "max": 99999999, + "default": 6000, + "label": "Request Intervall", + "help": "in milliseconds (10000-99999999, default: 60000 = 1 Min.)", + "sm": 12, + "md": 7, + "lg": 3 + }, "restCommandLogAsDebug": { "type": "checkbox", "label": "Successful commands as debug in log", "help": "Log '... Command xxx successfully set to x' as 'debug' and not 'info'", "default": false, "sm": 12, - "md": 7, + "md": 8, "lg": 5 } } diff --git a/io-package.json b/io-package.json index 7e98daa..ddc5ceb 100644 --- a/io-package.json +++ b/io-package.json @@ -163,7 +163,8 @@ "cancel" ] } - ] + ], + "installedFrom": "arteck/ioBroker.fullybrowser#newFully" }, "native": { "tableDevices": [], @@ -209,4 +210,4 @@ "native": {} } ] -} +} \ No newline at end of file diff --git a/lib/mqtt-server.js b/lib/mqtt-server.js index 8a9b5fd..c3ba06e 100644 --- a/lib/mqtt-server.js +++ b/lib/mqtt-server.js @@ -16,39 +16,50 @@ class MqttServer { start() { try { this.port = this.adapter.config.mqttPort; - if (this.adapter.adapterDir.includes("/.dev-server/default/node_modules")) { - this.port = 3012; - 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.`); - } + 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; } - if (!this.devices[client.id]) - this.devices[client.id] = {}; - let ip = void 0; + + // 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; + 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 === void 0; + ip = undefined; } - if (ip && !Object.keys(this.adapter.fullysEnbl).includes(ip)) { + + if (ip && !Object.keys(this.adapter.fullysMQTT).includes(ip)) { this.adapter.log.error(`[MQTT] Client ${client.id} not authorized: ${ip} is not an active Fully device IP per adapter settings.`); this.notAuthorizedClients.push(client.id); callback(null, false); return; } - const ipMsg = ip ? `${this.adapter.fullysEnbl[ip].name} (${ip})` : `${client.id} (IP unknown)`; + + 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 (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.`); @@ -61,6 +72,7 @@ class MqttServer { return; } } + this.adapter.log.info(`\u{1F511} MQTT Client ${ipMsg} successfully authenticated.`); callback(null, true); } catch (e) { @@ -68,14 +80,17 @@ class MqttServer { 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.fullysEnbl[ip].name} (${ip})` : `${client.id} (IP unknown)`; + 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"); @@ -90,6 +105,7 @@ class MqttServer { try { if (!client || !packet) return; + this.setIsAlive(client.id, true, "client published message"); if (!this.devices[client.id]) this.devices[client.id] = {}; @@ -102,29 +118,30 @@ class MqttServer { return; } const ip = info.ip4; - const devMsg = `${this.adapter.fullysEnbl[ip].name} (${ip})`; - if (!Object.keys(this.adapter.fullysEnbl).includes(ip)) { + 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 * 1e3; + 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 / 1e3)}s) ago...`); + 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.fullysEnbl[ip].name} = ${ip}`); + 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: ip, topic: packet.topic, infoObj: info }; @@ -132,7 +149,14 @@ class MqttServer { 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; @@ -159,7 +183,7 @@ class MqttServer { cmd: msg.event }; if (!this.devices[client.id].mqttFirstReceived) { - this.adapter.log.info(`[MQTT] \u{1F517} Client ${client.id} = ${this.adapter.fullysEnbl[ip].name} (${ip})`); + 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); @@ -173,7 +197,7 @@ class MqttServer { }); this.aedes.on("clientDisconnect", (client) => { const ip = this.devices[client.id].ip; - const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id; + const logMsgName = ip ? this.adapter.fullysMQTT[ip].name : client.id; if (this.adapter.config.mqttConnErrorsAsInfo) { this.adapter.log.info(`MQTT Client ${logMsgName} disconnected.`); } else { @@ -185,7 +209,7 @@ class MqttServer { if (this.notAuthorizedClients.includes(client.id)) return; const ip = this.devices[client.id].ip; - const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id; + 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 { @@ -196,7 +220,7 @@ class MqttServer { }); this.aedes.on("connectionError", (client, e) => { const ip = this.devices[client.id].ip; - const logMsgName = ip ? this.adapter.fullysEnbl[ip].name : client.id; + 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 { @@ -220,18 +244,20 @@ class MqttServer { } } setIsAlive(clientId, isAlive, msg) { - var _a; - if (isAlive) - this.devices[clientId].lastTimeActive = Date.now(); + + if (isAlive) this.devices[clientId].lastTimeActive = Date.now(); this.devices[clientId].isActive = isAlive; - const ip = (_a = this.devices[clientId]) == null ? void 0 : _a.ip; + + const ip = this.devices[clientId]?.ip; if (ip) { + // Call Adapter function onMqttAliveChange() this.adapter.onMqttAlive(ip, isAlive, msg); if (isAlive) { - this.scheduleCheckIfStillActive(clientId); + this.scheduleCheckIfStillActive(clientId); // restart timer } else { - if (this.devices[clientId].timeoutNoUpdate) - this.adapter.clearTimeout(this.devices[clientId].timeoutNoUpdate); + // 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.`); @@ -240,25 +266,29 @@ class MqttServer { async scheduleCheckIfStillActive(clientId) { try { const ip = this.devices[clientId].ip; - const ipMsg = ip ? `${this.adapter.fullysEnbl[ip].name} (${ip})` : `${clientId} (IP unknown)`; + 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 * 1e3; + + 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 > 7e4) { - this.adapter.log.debug(`[MQTT] ${ipMsg} NOT ALIVE - last contact ${Math.round(diff / 1e3)}s (${diff}ms) ago`); + 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 / 1e3)}s (${diff}ms) ago`); - this.setIsAlive(clientId, true, `alive check is successful (last contact: ${Math.round(diff / 1e3)}s 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) { diff --git a/lib/restApi.js b/lib/restApi.js index fd4e2af..c3b51f0 100644 --- a/lib/restApi.js +++ b/lib/restApi.js @@ -1,19 +1,20 @@ 'use strict'; -const import_axios = require('axios'); +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=" } + 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) { @@ -36,72 +37,133 @@ class RestApiFully { return false; } } + async axiosSendCmd(device, cmd, urlParam) { - var _a, _b, _c; + // 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 + method: 'get', + timeout: this.adapter.config.restTimeout, }; + try { - let urlHiddenPassword = url; - urlHiddenPassword = urlHiddenPassword.replace(/password=.*&type/g, "password=(hidden)&type"); - this.adapter.log.debug(`[REST] ${device.name}: Start sending command ${cmd}, URL: ${urlHiddenPassword}`); - const response = await import_axios.default.get(url, config); + // 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)) { + 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)) { + 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)) { + + 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": + 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") { + 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) { - const errTxt = `[REST] ${device.name}: Sending command ${cmd} failed`; - if (import_axios.default.isAxiosError(err)) { - if (!(err == null ? void 0 : err.response)) { - this.adapter.log.warn(`${errTxt}: No response`); - } else if (((_a = err.response) == null ? void 0 : _a.status) === 400) { - this.adapter.log.error("${errTxt}: Login Failed - Error 400 - " + ((_b = err.response) == null ? void 0 : _b.statusText)); - } else if ((_c = err.response) == null ? void 0 : _c.status) { - this.adapter.log.error(`${errTxt}: ${err.response.status} - ${err.response.statusText}`); - } else { - this.adapter.log.error(`${errTxt}: General Error`); - } + 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}: Error: ${this.adapter.err2Str(err)}`); + this.adapter.log.error(`${errTxt}: General Error`); } - return false; + } 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) { + + const url = `${device.restProtocol}://${device.ip}:${device.restPort}/?cmd=deviceInfo&type=json&password=${this.encodePassword(device.restPassword)}`; + + // Axios config + const config = { + method: 'get', + timeout: this.adapter.config.restTimeout, + }; + + try { + // Axios: Send command + const response = await axios.get(url, config); + + if (response.status == 200) { + + const result = { + clientId: 9999, + ip: device.ip, + topic: "fake", + infoObj: response.data + }; + + this.adapter.onMqttInfo(result); + } + } catch (err) { + const cmd = 'get axiosGetDevicesInfo '; + this.errorFunction(err,device, cmd); + return false; + } + } + + async startIntervall() { + try { + for (const ip in this.adapter.fullysRESTApi) { + this.axiosGetDevicesInfo(this.adapter.fullysRESTApi[ip]); + } + if (!this.adapter._requestInterval) { + this.adapter._requestInterval = setInterval(async () => { + await this.startIntervall(); + }, this.adapter.config.restIntervall); + } + } catch (error) { + + } + + } + + +} module.exports = RestApiFully; diff --git a/main.js b/main.js index febcbbe..8276024 100644 --- a/main.js +++ b/main.js @@ -5,7 +5,6 @@ const cmdsSwitches = require("./lib/constants").cmdsSwitches; const mqttEvents = require("./lib/constants").mqttEvents; const _methods = require("./lib/methods"); const MqttServer = require("./lib/mqtt-server"); -const AxiosCommand = require("./lib/axiosCommand"); const RestApiFully = require("./lib/restApi"); /** * ------------------------------------------------------------------- @@ -25,12 +24,17 @@ class fullybrowserControll extends utils.Adapter { this.getConfigValuePerKey = _methods.getConfigValuePerKey.bind(this); this.isIpAddressValid = _methods.isIpAddressValid.bind(this); - this.restApi_inst = new RestApiFully(this); + this.restApi = new RestApiFully(this); this.fullysEnbl = {}; + this.fullysMQTT = {}; + this.fullysRESTApi = {}; + 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)); @@ -49,25 +53,38 @@ class fullybrowserControll extends utils.Adapter { 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) + if (res) { await this.subscribeStatesAsync(this.fullysEnbl[ip].id + ".Commands.*"); + } 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 }); } + + // 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 @@ -78,9 +95,7 @@ class fullybrowserControll extends utils.Adapter { this.mqtt_Server = new MqttServer(this); this.mqtt_Server.start(); - this.axiosCommand = new AxiosCommand(this); - this.axiosCommand.start(); - + this.restApi.startIntervall(); this.deleteRemovedDeviceObjects(); } catch (e) { @@ -92,27 +107,27 @@ class fullybrowserControll extends utils.Adapter { async onMqttAlive(ip, isAlive, msg) { try { - const prevIsAlive = this.fullysEnbl[ip].isAlive; - this.fullysEnbl[ip].isAlive = isAlive; + const prevIsAlive = this.fullysMQTT[ip].isAlive; + this.fullysMQTT[ip].isAlive = isAlive; const calledBefore = this.onMqttAlive_EverBeenCalledBefore; this.onMqttAlive_EverBeenCalledBefore = true; if (!calledBefore && isAlive === true || prevIsAlive !== isAlive) { - this.setState(this.fullysEnbl[ip].id + ".alive", { + this.setState(this.fullysMQTT[ip].id + ".alive", { val: isAlive, ack: true }); if (isAlive) { - this.log.info(`${this.fullysEnbl[ip].name} is alive (MQTT: ${msg})`); + this.log.info(`${this.fullysMQTT[ip].name} is alive (MQTT: ${msg})`); } else { - this.log.warn(`${this.fullysEnbl[ip].name} is not alive! (MQTT: ${msg})`); + this.log.warn(`${this.fullysMQTT[ip].name} is not alive! (MQTT: ${msg})`); } } else { } let countAll = 0; let countAlive = 0; - for (const lpIpAddr in this.fullysEnbl) { + for (const lpIpAddr in this.fullysMQTT) { countAll++; - if (this.fullysEnbl[lpIpAddr].isAlive) { + if (this.fullysMQTT[lpIpAddr].isAlive) { countAlive++; } } @@ -131,9 +146,8 @@ class fullybrowserControll extends utils.Adapter { async onMqttInfo(obj) { try { - this.log.debug(`[MQTT] ${this.fullysEnbl[obj.ip].name} published info, topic: ${obj.topic}`); - const formerInfoKeysLength = this.fullysEnbl[obj.ip].mqttInfoKeys.length; const newInfoKeysAdded = []; + for (const key in obj.infoObj) { const val = obj.infoObj[key]; const valType = typeof val; @@ -144,7 +158,9 @@ class fullybrowserControll extends utils.Adapter { 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: { @@ -158,10 +174,6 @@ class fullybrowserControll extends utils.Adapter { }); } } - if (formerInfoKeysLength === 0) - this.log.debug(`[MQTT] ${this.fullysEnbl[obj.ip].name}: Initially create states for ${newInfoKeysAdded.length} info items (if not yet existing)`); - 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(", ")}`); for (const key in obj.infoObj) { const newVal = typeof obj.infoObj[key] === "object" ? JSON.stringify(obj.infoObj[key]) : obj.infoObj[key]; @@ -196,10 +208,10 @@ class fullybrowserControll extends utils.Adapter { async onMqttEvent(obj) { try { - this.log.debug(`[MQTT] \u{1F4E1} ${this.fullysEnbl[obj.ip].name} published event, topic: ${obj.topic}, cmd: ${obj.cmd}`); - const pthEvent = `${this.fullysEnbl[obj.ip].id}.Events.${obj.cmd}`; + 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.fullysEnbl[obj.ip].name}: Event ${obj.cmd} received but state ${pthEvent} does not exist, so we create it first`); + 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: { @@ -216,7 +228,7 @@ class fullybrowserControll extends utils.Adapter { val: true, ack: true }); - const pthCmd = this.fullysEnbl[obj.ip].id + ".Commands"; + const pthCmd = this.fullysMQTT[obj.ip].id + ".Commands"; const idx = this.getIndexFromConf(cmdsSwitches, ["mqttOn", "mqttOff"], obj.cmd); if (idx !== -1) { const conf = cmdsSwitches[idx]; @@ -241,7 +253,7 @@ class fullybrowserControll extends utils.Adapter { ack: true }); } else { - 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`); + 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`); } } } catch (e) { @@ -261,11 +273,14 @@ class fullybrowserControll extends utils.Adapter { 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); @@ -276,9 +291,12 @@ class fullybrowserControll extends utils.Adapter { if (!stateObj.val) return; } + if (!cmdToSend) throw `onStateChange() - ${stateId}: fullyCmd could not be determined!`; - const sendCommand = await this.restApi_inst.sendCmd(fully, cmdToSend, stateObj.val); + + 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}`); @@ -365,6 +383,7 @@ class fullybrowserControll extends utils.Adapter { 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)) { @@ -424,7 +443,8 @@ class fullybrowserControll extends utils.Adapter { restPort: 0, restPassword: "", lastSeen: 0, - isAlive: false + isAlive: false, + apiType: "" }; if (!this.isIpAddressValid(lpDevice.ip)) { this.log.error(`${finalDevice.name}: Provided IP address "${lpDevice.ip}" is not valid!`); @@ -434,6 +454,7 @@ class fullybrowserControll extends utils.Adapter { this.log.error(`Provided device name "${lpDevice.name}" is empty!`); return false; } + finalDevice.name = lpDevice.ip.trim(); finalDevice.id = this.cleanDeviceName(lpDevice.name); @@ -441,12 +462,21 @@ class fullybrowserControll extends utils.Adapter { this.log.error(`Provided device name "${lpDevice.name}" is too short and/or has invalid characters!`); return false; } + + 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 (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.restProtocol !== "http" && lpDevice.restProtocol !== "https") { this.log.warn(`${finalDevice.name}: REST API Protocol is empty, set to http as default.`); finalDevice.restProtocol = "http"; @@ -476,20 +506,33 @@ class fullybrowserControll extends utils.Adapter { } else { finalDevice.restPassword = lpDevice.restPassword; } + finalDevice.enabled = lpDevice.enabled ? true : false; + const logConfig = { ...finalDevice }; + logConfig.restPassword = "(hidden)"; this.log.debug(`Final Config: ${JSON.stringify(logConfig)}`); this.fullysAll[finalDevice.ip] = finalDevice; + if (lpDevice.enabled) { this.fullysEnbl[finalDevice.ip] = finalDevice; + + 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.`); @@ -557,6 +600,17 @@ class fullybrowserControll extends utils.Adapter { }, 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: { @@ -589,28 +643,34 @@ class fullybrowserControll extends utils.Adapter { native: {} }); } - 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: {} - }); + + 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)); diff --git a/package-lock.json b/package-lock.json index e94ff03..5d7ee35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,36 +1,36 @@ { "name": "iobroker.fullybrowser", - "version": "2.2.0", + "version": "3.0.0-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "iobroker.fullybrowser", - "version": "2.2.0", + "version": "3.0.0-beta.0", "license": "MIT", "dependencies": { - "@iobroker/adapter-core": "^2.6.8", + "@iobroker/adapter-core": "^3.0.3", "aedes": "^0.48.1", "axios": "^1.5.1", "net": "^1.0.2" }, "devDependencies": { - "@alcalzone/release-script": "^3.5.9", - "@alcalzone/release-script-plugin-iobroker": "^3.5.9", + "@alcalzone/release-script": "^3.6.0", + "@alcalzone/release-script-plugin-iobroker": "^3.6.0", "@alcalzone/release-script-plugin-license": "^3.5.9", "@alcalzone/release-script-plugin-manual-review": "^3.5.9", "@iobroker/adapter-dev": "^1.2.0", "@iobroker/testing": "^4.1.0", "@types/chai": "^4.3.4", - "@types/chai-as-promised": "^7.1.5", - "@types/mocha": "^10.0.1", + "@types/chai-as-promised": "^7.1.6", + "@types/mocha": "^10.0.3", "@types/node": "^18.15.11", "@types/proxyquire": "^1.3.28", "@types/sinon": "^10.0.13", "@types/sinon-chai": "^3.2.9", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", - "chai": "^4.3.7", + "chai": "^4.3.10", "chai-as-promised": "^7.1.1", "eslint": "^8.37.0", "eslint-config-prettier": "^8.8.0", @@ -43,10 +43,10 @@ "sinon-chai": "^3.7.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.1", - "typescript": "~5.0.3" + "typescript": "~5.2.2" }, "engines": { - "node": ">=12" + "node": ">=16" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -597,11 +597,14 @@ "dev": true }, "node_modules/@iobroker/adapter-core": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/@iobroker/adapter-core/-/adapter-core-2.6.8.tgz", - "integrity": "sha512-xrqtH5RYZ6BvEcDyfuPkajd9el4R6p0VLRYKlnfMafAbxybIN+zfeHvjGI4l8OAHkyP2tcv6boX2Vu0KnMFOHw==", - "dependencies": { - "@types/iobroker": "^4.0.5" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@iobroker/adapter-core/-/adapter-core-3.0.4.tgz", + "integrity": "sha512-QsSeIkOa+zEVdIQ0kc0GcfsnQC+pTWWkixotdG4naKuaLUwMWK7xP0UyUEUiaHfPG3KqyZxwIxQR9HMENvvIYQ==", + "engines": { + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@iobroker/types": "^5.0.11" } }, "node_modules/@iobroker/adapter-dev": { @@ -710,6 +713,15 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/@iobroker/types": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/@iobroker/types/-/types-5.0.15.tgz", + "integrity": "sha512-fmYUX9p8U5i5P3Mfa+kCbhJtBSDLsggZVVyU71sb8kE38wdPFZeyi3ciiIII5n8r+LrWQ9dUVqomJSv2sMnNkg==", + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -948,14 +960,6 @@ "@types/node": "*" } }, - "node_modules/@types/iobroker": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/iobroker/-/iobroker-4.0.5.tgz", - "integrity": "sha512-D1tJwuDQEQQQ/cZVFjFjFUhUuMxJbfrz5U2UooiZwhgs69D/t8IowMvBI6Lk4ZR8HnCSxYwWHVRKyQnEMNgJPA==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -1006,6 +1010,7 @@ "version": "18.18.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.7.tgz", "integrity": "sha512-bw+lEsxis6eqJYW8Ql6+yTqkE6RuFtsQPSe5JxXbqYRFQEER5aJA9a5UH9igqDWm3X4iLHIKOHlnAXLM4mi7uQ==", + "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -5181,16 +5186,16 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/uc.micro": { @@ -5220,7 +5225,8 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "node_modules/universalify": { "version": "2.0.0", @@ -5833,12 +5839,10 @@ "dev": true }, "@iobroker/adapter-core": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/@iobroker/adapter-core/-/adapter-core-2.6.8.tgz", - "integrity": "sha512-xrqtH5RYZ6BvEcDyfuPkajd9el4R6p0VLRYKlnfMafAbxybIN+zfeHvjGI4l8OAHkyP2tcv6boX2Vu0KnMFOHw==", - "requires": { - "@types/iobroker": "^4.0.5" - } + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@iobroker/adapter-core/-/adapter-core-3.0.4.tgz", + "integrity": "sha512-QsSeIkOa+zEVdIQ0kc0GcfsnQC+pTWWkixotdG4naKuaLUwMWK7xP0UyUEUiaHfPG3KqyZxwIxQR9HMENvvIYQ==", + "requires": {} }, "@iobroker/adapter-dev": { "version": "1.2.0", @@ -5940,6 +5944,12 @@ } } }, + "@iobroker/types": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/@iobroker/types/-/types-5.0.15.tgz", + "integrity": "sha512-fmYUX9p8U5i5P3Mfa+kCbhJtBSDLsggZVVyU71sb8kE38wdPFZeyi3ciiIII5n8r+LrWQ9dUVqomJSv2sMnNkg==", + "peer": true + }, "@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -6162,14 +6172,6 @@ "@types/node": "*" } }, - "@types/iobroker": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/iobroker/-/iobroker-4.0.5.tgz", - "integrity": "sha512-D1tJwuDQEQQQ/cZVFjFjFUhUuMxJbfrz5U2UooiZwhgs69D/t8IowMvBI6Lk4ZR8HnCSxYwWHVRKyQnEMNgJPA==", - "requires": { - "@types/node": "*" - } - }, "@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -6220,6 +6222,7 @@ "version": "18.18.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.7.tgz", "integrity": "sha512-bw+lEsxis6eqJYW8Ql6+yTqkE6RuFtsQPSe5JxXbqYRFQEER5aJA9a5UH9igqDWm3X4iLHIKOHlnAXLM4mi7uQ==", + "dev": true, "requires": { "undici-types": "~5.26.4" } @@ -9188,9 +9191,9 @@ "dev": true }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true }, "uc.micro": { @@ -9214,7 +9217,8 @@ "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "universalify": { "version": "2.0.0",