diff --git a/.eslintrc.json b/.eslintrc.json index 5eff06f2..1fa9d438 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,13 @@ } ], "no-console": "off", + "no-unused-vars": [ + "error", + { + "ignoreRestSiblings": true, + "argsIgnorePattern": "^_" + } + ], "no-var": "error", "no-trailing-spaces": "error", "prefer-const": "error", @@ -31,6 +38,6 @@ ] }, "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2022 } } \ No newline at end of file diff --git a/lib/request.js b/lib/request.js deleted file mode 100644 index bc8a5436..00000000 --- a/lib/request.js +++ /dev/null @@ -1,92 +0,0 @@ -const axios = require('axios').default; - -/** - * @param {ioBroker.Adapter} adapter - * @param {string} url - * @param {{}} headers - * @return {Promise} - */ -async function getRequest(adapter, url, headers = {}) { - return await _sendRequest(adapter, url, null, 'GET', headers); -} - -/** - * @param {ioBroker.Adapter} adapter - * @param {string} url - * @param {string | {}} body - * @param {{}} headers - * @return {Promise} - */ -async function postRequest(adapter, url, body, headers = {}) { - return await _sendRequest(adapter, url, body, 'POST', headers); -} - -/** - * @param {ioBroker.Adapter} adapter - * @param {string} url - * @param {{}} headers - * @return {Promise} - */ -async function deleteRequest(adapter, url, headers = {}) { - return await _sendRequest(adapter, url, null, 'DELETE', headers); -} - -/** - * @param {ioBroker.Adapter} adapter - * @param {string} url - * @param {any} body - * @param {axios.Method} method - * @param {{}} headers - * @return {Promise} - */ -async function _sendRequest(adapter, url, body, method, headers = {}) { - // Content-Type ergänzen falls nicht vorhanden - if (!Object.keys(headers).includes('Content-Type')) { - headers['Content-Type'] = 'application/json'; - } - - adapter.log.debug(`[_sendRequest.${method}] url="${url}", body="${body !== undefined ? JSON.stringify(body) : 'null'}", headers="${JSON.stringify(headers)}"`); - - let response; - try { - response = await axios({url: url, method: method, headers: headers, data: body}); - } catch (/** @type {axios.AxiosError | Error} */ e) { - // Logging - let message; - if (e instanceof axios.AxiosError) { - /** @type {object} */ - const json = e.toJSON(); - message = `code="${e.code}", status=${json.status}, config="${JSON.stringify(json.config)}", stack="${json.stack}"`; - } else { - message = `stack="${e.stack}"`; - } - - adapter.log.debug(`[_sendRequest.${method}] ${e.name}: "${e.message}", ${message}`); - - // Error handling - if (e instanceof axios.AxiosError && e.response && e.response.status !== 200) { - throw Error(`HTTP Error (${e.response.status}) ${e.response.statusText}${e.response.data ? ': ' + JSON.stringify(e.response.data) : ''}`); - } else { - throw Error(e.message ? e.message : e); - } - } - - if (typeof response === 'undefined') { - throw Error('No response received'); - } - - if (response.status !== 200) { - throw Error(`HTTP Error (${response.status}) ${response.statusText}${response.data ? ': ' + JSON.stringify(response.data) : ''}`); - } else { - if (response.data) { - adapter.log.debug(`[_sendRequest.${method}.response] ${JSON.stringify(response.data)}`); - } - return response.data; - } -} - -module.exports = { - getRequest, - postRequest, - deleteRequest -}; \ No newline at end of file diff --git a/lib/twinkly.js b/lib/twinkly.js index 53180ec3..ab1aa54d 100644 --- a/lib/twinkly.js +++ b/lib/twinkly.js @@ -1,7 +1,8 @@ -const request = require('./request'); const ping = require('./ping'); const tools = require('./tools'); const crypto = require('crypto'); +// const dgram = require('dgram'); +const axios = require('axios').default; const HTTPCodes = { values : { @@ -56,8 +57,44 @@ const STATE_ON_LASTMODE = 'lastMode'; const TOKEN_VERIFICATION_FAILED = `(${HTTPCodes.values.error}) Verification failed!`; -class Twinkly { +// UDP port to send realtime frames to +// const REALTIME_UDP_PORT_NUMBER = 7777; + +// https://github.com/jschlyter/ttls/blob/main/ttls/client.py#L58 +const TWINKLY_MUSIC_DRIVERS_OFFICIAL = { + 'VU Meter': '00000000-0000-0000-0000-000000000001', + 'Beat Hue': '00000000-0000-0000-0000-000000000002', + 'Psychedelica': '00000000-0000-0000-0000-000000000003', + 'Red Vertigo': '00000000-0000-0000-0000-000000000004', + 'Dancing Bands': '00000000-0000-0000-0000-000000000005', + 'Diamond Swirl': '00000000-0000-0000-0000-000000000006', + 'Joyful Stripes': '00000000-0000-0000-0000-000000000007', + 'Angel Fade': '00000000-0000-0000-0000-000000000008', + 'Clockwork': '00000000-0000-0000-0000-000000000009', + 'Sipario': '00000000-0000-0000-0000-00000000000A', + 'Sunset': '00000000-0000-0000-0000-00000000000B', + 'Elevator': '00000000-0000-0000-0000-00000000000C', +}; + +// https://github.com/jschlyter/ttls/blob/main/ttls/client.py#L73 +const TWINKLY_MUSIC_DRIVERS_UNOFFICIAL = { + 'VU Meter 2': '00000000-0000-0000-0000-000001000001', + 'Beat Hue 2': '00000000-0000-0000-0000-000001000002', + 'Psychedelica 2': '00000000-0000-0000-0000-000001000003', + 'Sparkle': '00000000-0000-0000-0000-000001000005', + 'Sparkle Hue': '00000000-0000-0000-0000-000001000006', + 'Psycho Sparkle': '00000000-0000-0000-0000-000001000007', + 'Psycho Hue': '00000000-0000-0000-0000-000001000008', + 'Red Line': '00000000-0000-0000-0000-000001000009', + 'Red Vertigo 2': '00000000-0000-0000-0000-000002000004', + 'Dancing Bands 2': '00000000-0000-0000-0000-000002000005', + 'Diamond Swirl 2': '00000000-0000-0000-0000-000002000006', + 'Angel Fade 2': '00000000-0000-0000-0000-000002000008', + 'Clockwork 2': '00000000-0000-0000-0000-000002000009', + 'Sunset 2': '00000000-0000-0000-0000-00000200000B', +}; +class Twinkly { /** * * @param {ioBroker.Adapter} adapter @@ -70,7 +107,14 @@ class Twinkly { this.name = name; this.host = host; this.onDataChange = onDataChange; - this.resetToken(); + + // Axios + this.axiosInstance = axios.create({ + baseURL : this.base(), + headers : { + 'Content-Type': 'application/json' + } + }); this.connected = false; this.firmware = '0.0.0'; @@ -91,6 +135,8 @@ class Twinkly { this.ledMovies = {}; /** @type {{[p: String]: String}} */ this.playlist = {}; + + this.resetToken(); } /** @@ -98,7 +144,7 @@ class Twinkly { * @return {String} */ base() { - return `http://${this.host}/xled/v1`; + return `http://${this.host}/xled/v1/`; } /** @@ -145,7 +191,7 @@ class Twinkly { /** * Token prüfen ob er bereits abgelaufen ist. * @param {boolean} force - * @return {Promise} + * @return {Promise} */ async ensure_token(force) { if (force || this.isTokenEmpty() || this.expires <= Date.now()) { @@ -164,8 +210,6 @@ class Twinkly { } else { this.adapter.log.debug(`[${this.name}.ensure_token] Authentication token still valid (${new Date(this.expires).toLocaleString()})`); } - - return this.token; } /** @@ -176,15 +220,10 @@ class Twinkly { async login() { this.resetToken(); - let response; - try { - const buffer = crypto.randomBytes(32); - response = await this._handleHttpTwinklyCodeCheck('POST', 'login', {challenge: buffer.toString('base64')}); - } catch (e) { - throw Error(e.message); - } + const buffer = crypto.randomBytes(32); + const response = await this._sendRequestTwinklyCodeCheck('POST', 'login', {challenge: buffer.toString('base64')}); - this.token = String(response['authentication_token']); + this._setToken(String(response['authentication_token'])); this.expires = Date.now() + (Number(response['authentication_token_expires_in']) * 1000); this.challengeResponse = String(response['challenge-response']); @@ -192,7 +231,7 @@ class Twinkly { authenticationToken : String(response['authentication_token']), authenticationTokenExpiresIn : Number(response['authentication_token_expires_in']), challengeResponse : String(response['challenge-response']), - code : Number(response['code']) + code : Number(response.code) }; } @@ -207,8 +246,7 @@ class Twinkly { } try { - const response = await this._handleHttpTwinklyCodeCheck('POST', 'verify', {'challenge-response': this.challengeResponse}); - return {code: response['code']}; + return await this._sendRequestTwinklyCodeCheck('POST', 'verify', {'challenge-response': this.challengeResponse}); } catch (e) { this.resetToken(); if (e.message.includes(`(${HTTPCodes.values.error})`)) { @@ -225,27 +263,23 @@ class Twinkly { * @see REST-API */ async logout() { - try { - let response; - if (!this.isTokenEmpty()) { - response = await this._post('logout', {}); - this.resetToken(); - } - - return {code: response ? response['code'] : HTTPCodes.values.ok}; - } catch (e) { - throw Error(e.message); + let response; + if (!this.isTokenEmpty()) { + response = await this._post('logout', {}); + this.resetToken(); } + + return {code: response ? response.code : HTTPCodes.values.ok}; } /** * Device details - * @return {Promise<{details: {base_leds_number: Number, bytes_per_led: Number, copyright: String, device_name: String, flash_size: Number, - * frame_rate: Number, fw_family: String, hardware_version: String, hw_id: String, led_profile: String, - * led_type: Number, mac: String, max_movies: Number, max_supported_led: Number, measured_frame_rate: Number, - * movie_capacity: Number, number_of_led: Number, production_site: Number, production_date: Number, - * product_name: String, product_code: String, serial: String, uid: String, uptime: String, uuid: String, - * wire_type: Number, group: {mode: String, compat_mode: Number, uid: String}}, + * @return {Promise<{base_leds_number: Number, bytes_per_led: Number, copyright: String, device_name: String, flash_size: Number, + * frame_rate: Number, fw_family: String, hardware_version: String, hw_id: String, led_profile: String, + * led_type: Number, mac: String, max_movies: Number, max_supported_led: Number, measured_frame_rate: Number, + * movie_capacity: Number, number_of_led: Number, production_site: Number, production_date: Number, + * product_name: String, product_code: String, serial: String, uid: String, uptime: String, uuid: String, + * wire_type: Number, group: {mode: String, compat_mode: Number, uid: String}, * code: Number}>} * @see REST-API */ @@ -253,45 +287,30 @@ class Twinkly { const oldDetails = this.details; this.details = {}; - try { - const param = tools.versionGreaterEquals('2.8.3', this.firmware) ? '?filter=prod_infos&filter2=group' : ''; - const response = await this._handleHttpTwinklyCodeCheck('GET', 'gestalt' + param, null); + const param = tools.versionGreaterEquals('2.8.3', this.firmware) ? '?filter=prod_infos&filter2=group' : ''; + const response = await this._sendRequestTwinklyCodeCheck('GET', 'gestalt' + param, null); - this._cloneTwinklyResponse(response, this.details); + tools.cloneObject(response, this.details); + delete this.details['code']; - // Check for changes after first run - if (Object.keys(oldDetails).length > 0) { - // Check if group mode changed - if (this.details.group) { - await this._checkDataChange('groupMode', this.details.group.mode, oldDetails.group ? oldDetails.group.mode : ''); - } + // Check for changes after first run + if (Object.keys(oldDetails).length > 0) { + // Check if group mode changed + if (this.details.group) { + await this._checkDataChange('groupMode', this.details.group.mode, oldDetails.group ? oldDetails.group.mode : ''); } - - return {details: this.details, code: response['code']}; - } catch (e) { - throw Error(e.message); } + + return response; } /** * Get Device name - * @return {Promise<{name: {name: String}, code: Number}>} + * @return {Promise<{name: String, code: Number}>} * @see REST-API */ async getDeviceName() { - try { - const response = await this._get('device_name'); - - /** - * @type {{name: String}} - */ - const name = {}; - this._cloneTwinklyResponse(response, name); - - return {name: name, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('device_name'); } /** @@ -301,12 +320,7 @@ class Twinkly { * @see REST-API */ async setDeviceName(name) { - try { - const response = await this._post('device_name', {name: name}); - return {name: response['name'], code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('device_name', {name: name}); } /** @@ -316,33 +330,17 @@ class Twinkly { * @see REST-API */ async echo(message) { - try { - const response = await this._post('echo', {message: message}); - return {message: response['json']['message'], code: response['code']}; - } catch (e) { - throw Error(e.message); - } + const response = await this._post('echo', {message: message}); + return {message: response.json.message, code: response.code}; } /** * Get timer - * @return {Promise<{timer: {time_now: Number, time_on: Number, time_off: Number, tz: String}, code: Number}>} + * @return {Promise<{time_now: Number, time_on: Number, time_off: Number, tz: String, code: Number}>} * @see REST-API */ async getTimer() { - try { - const response = await this._get('timer'); - - /** - * @type {{time_now: Number, time_on: Number, time_off: Number, tz: String}} - */ - const timer = {}; - this._cloneTwinklyResponse(response, timer); - - return {timer: timer, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('timer'); } /** @@ -352,35 +350,16 @@ class Twinkly { * @see REST-API */ async setTimer(data) { - try { - data = typeof data === 'string' ? JSON.parse(data) : data; - - const response = await this._post('timer', data); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('timer', typeof data === 'string' ? JSON.parse(data) : data); } /** * Get Layout - * @return {Promise<{layout: {aspectXY: Number, aspectXZ: Number, coordinates: {x: Number, y: Number, z: Number}[], source: String, synthesized: Boolean, uuid: String}, code: Number}>} + * @return {Promise<{aspectXY: Number, aspectXZ: Number, coordinates: {x: Number, y: Number, z: Number}[], source: String, synthesized: Boolean, uuid: String, code: Number}>} * @see REST-API */ async getLayout() { - try { - const response = await this._get('led/layout/full'); - - /** - * @type {{aspectXY: Number, aspectXZ: Number, coordinates: {x: Number, y: Number, z: Number}[], source: String, synthesized: Boolean, uuid: String}} - */ - const layout = {}; - this._cloneTwinklyResponse(response, layout); - - return {layout: layout, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('led/layout/full'); } /** @@ -394,39 +373,27 @@ class Twinkly { * @see REST-API */ async uploadLayout(aspectXY, aspectXZ, coordinates, source, synthesized) { - try { - const response = await this._post('led/layout/full', - {aspectXY: aspectXY, aspectXZ: aspectXZ, coordinates: coordinates, source: source, synthesized: synthesized}); - - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/layout/full', + {aspectXY: aspectXY, aspectXZ: aspectXZ, coordinates: coordinates, source: source, synthesized: synthesized}); } /** * Get LED operation mode - * @return {Promise<{mode: {mode: String, shop_mode: Number, id: Number, unique_id: String, name: String}, code: Number}>} + * @return {Promise<{mode: String, shop_mode: Number, id: Number, unique_id: String, name: String, code: Number}>} * @see REST-API */ async getLEDMode() { try { const response = await this._get('led/mode'); - /** - * @type {{mode: String, shop_mode: Number, id: Number, unique_id: String, name: String}} - */ - const mode = {}; - this._cloneTwinklyResponse(response, mode); - const oldMode = this.ledMode; - this.ledMode = mode.mode; + this.ledMode = response.mode; - if (mode.mode !== '') { + if (response.mode !== '') { await this._checkDataChange('ledMode', this.ledMode, oldMode); } - return {mode: mode, code: response['code']}; + return response; } catch (e) { this.ledMode = lightModes.value.off; throw Error(e.message); @@ -440,33 +407,16 @@ class Twinkly { * @see REST-API */ async setLEDMode(mode) { - try { - const response = await this._post('led/mode', {mode: mode}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/mode', {mode: mode}); } /** * Get LED color - * @return {Promise<{color: {hue: Number, saturation: Number, value: Number, red: Number, green: Number, blue: Number, white: Number}, code: Number}>} + * @return {Promise<{hue: Number, saturation: Number, value: Number, red: Number, green: Number, blue: Number, white: Number, code: Number}>} * @see REST-API */ async getLEDColor() { - try { - const response = await this._get('led/color'); - - /** - * @type {{hue: Number, saturation: Number, value: Number, red: Number, green: Number, blue: Number, white: Number}} - */ - const color = {}; - this._cloneTwinklyResponse(response, color); - - return {color: color, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('led/color'); } /** @@ -478,12 +428,7 @@ class Twinkly { * @see REST-API */ async setLEDColorHSV(hue, sat, value) { - try { - const response = await this.setLEDColor({hue: hue, saturation: sat, value: value}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setLEDColor({hue: hue, saturation: sat, value: value}); } /** @@ -496,18 +441,13 @@ class Twinkly { * @see REST-API */ async setLEDColorRGBW(red, green, blue, white) { - try { - /** @type {{red: Number, green: Number, blue: Number, white?: Number}} */ - const data = {red: red, green: green, blue: blue}; - if (white >= 0 && await this.checkDetailInfo({name: 'led_profile', val: 'RGBW'})) { - data.white = white; - } - - const response = await this.setLEDColor(data); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); + /** @type {{red: Number, green: Number, blue: Number, white?: Number}} */ + const data = {red: red, green: green, blue: blue}; + if (white >= 0 && await this.checkDetailInfo({name: 'led_profile', val: 'RGBW'})) { + data.white = white; } + + return await this.setLEDColor(data); } /** @@ -517,33 +457,16 @@ class Twinkly { * @see REST-API */ async setLEDColor(color) { - try { - const response = await this._post('led/color', color); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/color', color); } /** * Get LED effects - * @return {Promise<{effects: {effects_number: Number, unique_ids: String[]}, code: Number}>} + * @return {Promise<{effects_number: Number, unique_ids: String[], code: Number}>} * @see REST-API */ async getLEDEffects() { - try { - const response = await this._get('led/effects'); - - /** - * @type {{effects_number: Number, unique_ids: String[]}} - */ - const effects = {}; - this._cloneTwinklyResponse(response, effects); - - return {effects: effects, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('led/effects'); } /** @@ -553,35 +476,20 @@ class Twinkly { */ async getListOfLEDEffects() { this.ledEffects = {}; - try { - const response = await this.getLEDEffects(); - for (let effect = 0; effect < response.effects.effects_number; effect++) { - this.ledEffects[effect] = `Effect ${effect+1}`; - } - } catch (e) { - this.adapter.log.error(`[${this.name}.getListOfLEDEffects] Could not get effects! ${e.message}`); + + const response = await this.getLEDEffects(); + for (let effect = 0; effect < response.effects_number; effect++) { + this.ledEffects[effect] = `Effect ${effect+1}`; } } /** * Get current LED effect - @return {Promise<{effect: {effect_id: Number, preset_id: Number, unique_id: String}, code: Number}>} + * @return {Promise<{effect_id: Number, preset_id: Number, unique_id: String, code: Number}>} * @see REST-API */ async getCurrentLEDEffect() { - try { - const response = await this._get('led/effects/current'); - - /** - * @type {{effect_id: Number, preset_id: Number, unique_id: String}} - */ - const effect = {}; - this._cloneTwinklyResponse(response, effect); - - return {effect: effect, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('led/effects/current'); } /** @@ -591,34 +499,16 @@ class Twinkly { * @see REST-API */ async setCurrentLEDEffect(effectId) { - try { - const response = await this._post('led/effects/current', {effect_id: effectId}); - - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/effects/current', {effect_id: effectId}); } /** * Get LED config - * @return {Promise<{config: {strings: {first_led_id: Number, length: Number}[]}, code: Number}>} + * @return {Promise<{strings: {first_led_id: Number, length: Number}[], code: Number}>} * @see REST-API */ async getLEDConfig() { - try { - const response = await this._get('led/config'); - - /** - * @type {{strings: {first_led_id: Number, length: Number}[]}} - */ - const config = {}; - this._cloneTwinklyResponse(response, config); - - return {config: config, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('led/config'); } /** @@ -628,12 +518,7 @@ class Twinkly { * @see REST-API */ async setLEDConfig(strings) { - try { - const response = await this._post('led/config', {strings: strings}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/config', {strings: strings}); } /** @@ -644,46 +529,38 @@ class Twinkly { * @see REST-API */ async uploadMovie(frames, delay) { - try { - const movieFormat = this._convertMovieFormat(frames); - - // Switch off lights - let response = await this.setLEDMode('off'); - // Upload movie - if (response.code === HTTPCodes.values.ok) { - response = await this._post('led/movie/full', movieFormat.bufferArray, {'Content-Type': 'application/octet-stream'}); - } - // Update configuration - if (response.code === HTTPCodes.values.ok) { - response = await this.setLEDMovieConfig(delay, movieFormat.lightsCount, movieFormat.frameCount); - } - // Switch on lights - if (response.code === HTTPCodes.values.ok) { - response = await this.setLEDMode('movie'); - } + const movieFormat = this._convertMovieFormat(frames); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); + // Switch off lights + let response = await this.setLEDMode(lightModes.value.off); + // Upload movie + if (response.code === HTTPCodes.values.ok) { + response = await this._post('led/movie/full', movieFormat.bufferArray, {'Content-Type': 'application/octet-stream'}); + } + // Update configuration + if (response.code === HTTPCodes.values.ok) { + response = await this.setLEDMovieConfig(delay, movieFormat.lightsCount, movieFormat.frameCount); } + // Switch on lights + if (response.code === HTTPCodes.values.ok) { + response = await this.setLEDMode(lightModes.value.movie); + } + + return {code: response.code}; } /** * * @param {{r: Number, g: Number, b: Number}[][]} frames - * @returns {{lightsCount, frameCount: number, bufferArray: undefined}} + * @returns {{lightsCount, frameCount: number, bufferArray: ArrayBuffer}} * @private */ _convertMovieFormat (frames) { - const output = { - bufferArray : undefined, - frameCount : frames.length, - lightsCount : frames[0].length - }; + const lightsCount = frames[0].length; const fullArray = []; for (let x = 0; x < frames.length; x++) { - if (frames[x].length !== output.lightsCount) { + if (frames[x].length !== lightsCount) { throw new Error('Not all frames have the same number of lights!'); } for (let y = 0; y < frames[x].length; y++) { @@ -692,7 +569,12 @@ class Twinkly { fullArray.push(frames[x][y].b); } } - output.bufferArray = new ArrayBuffer(fullArray.length); + + const output = { + bufferArray : new ArrayBuffer(fullArray.length), + frameCount : frames.length, + lightsCount : lightsCount + }; const longInt8View = new Uint8Array(output.bufferArray); for (let x = 0; x < fullArray.length; x++) { @@ -740,27 +622,13 @@ class Twinkly { /** * Get LED movie config - * @return {Promise<{movie: {frame_delay: Number, leds_number: Number, loop_type: Number, frames_number: Number, - * sync?: {mode: String, slave_id: String, master_id: String, compat_mode: Number}, - * mic?: {filters: {}[], brightness_depth: Number, hue_depth: Number, value_depth: Number, saturation_depth: Number}}, code: Number}>} + * @return {Promise<{frame_delay: Number, leds_number: Number, loop_type: Number, frames_number: Number, + * sync?: {mode: String, slave_id: String, master_id: String, compat_mode: Number}, + * mic?: {filters: {}[], brightness_depth: Number, hue_depth: Number, value_depth: Number, saturation_depth: Number}, code: Number}>} * @see REST-API */ async getLEDMovieConfig() { - try { - const response = await this._get('led/movie/config'); - - /** - * @type {{frame_delay: Number, leds_number: Number, loop_type: Number, frames_number: Number, - * sync?: {mode: String, slave_id: String, master_id: String, compat_mode: Number}, - * mic?: {filters: {}[], brightness_depth: Number, hue_depth: Number, value_depth: Number, saturation_depth: Number}}} - */ - const movie = {}; - this._cloneTwinklyResponse(response, movie); - - return {movie: movie, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('led/movie/config'); } /** @@ -772,33 +640,16 @@ class Twinkly { * @see REST-API */ async setLEDMovieConfig(delay, leds, frames) { - try { - const response = await this._post('led/movie/config', {frame_delay : delay, leds_number : leds, frames_number : frames}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/movie/config', {frame_delay : delay, leds_number : leds, frames_number : frames}); } /** * Get brightness - * @return {Promise<{bri: {value: Number, mode: String}, code: Number}>} + * @return {Promise<{value: Number, mode: String, code: Number}>} * @see REST-API */ async getBrightness() { - try { - const response = await this._get('led/out/brightness'); - - /** - * @type {{value: Number, mode: String}} - */ - const bri = {}; - this._cloneTwinklyResponse(response, bri); - - return {bri: bri, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('led/out/brightness'); } /** @@ -808,12 +659,7 @@ class Twinkly { * @see REST-API */ async setBrightnessAbsolute(brightness) { - try { - const response = await this.setBrightness({value: brightness, mode: 'enabled', type: 'A'}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setBrightness({value: brightness, mode: 'enabled', type: 'A'}); } /** @@ -823,12 +669,7 @@ class Twinkly { * @see REST-API */ async setBrightnessRelative(brightness) { - try { - const response = await this.setBrightness({value: brightness, mode: 'enabled', type: 'R'}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setBrightness({value: brightness, mode: 'enabled', type: 'R'}); } /** @@ -837,12 +678,7 @@ class Twinkly { * @see REST-API */ async setBrightnessDisabled() { - try { - const response = await this.setBrightness({value: 0, mode: 'disabled', type: 'A'}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setBrightness({value: 0, mode: 'disabled', type: 'A'}); } /** @@ -852,31 +688,16 @@ class Twinkly { * @see REST-API */ async setBrightness(data) { - try { - const response = await this._post('led/out/brightness', data); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/out/brightness', data); } /** * Get saturation - * @return {Promise<{sat: {value: Number, mode: String}, code: Number}>} + * @return {Promise<{value: Number, mode: String, code: Number}>} * @see REST-API */ async getSaturation() { - try { - const response = await this._get('led/out/saturation'); - - /** @type {{value: Number, mode: String}} */ - const sat = {}; - this._cloneTwinklyResponse(response, sat); - - return {sat: sat, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('led/out/saturation'); } /** @@ -886,12 +707,7 @@ class Twinkly { * @see REST-API */ async setSaturationAbsolute(saturation) { - try { - const response = await this.setSaturation({value: saturation, mode: 'enabled', type: 'A'}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setSaturation({value: saturation, mode: 'enabled', type: 'A'}); } /** @@ -901,12 +717,7 @@ class Twinkly { * @see REST-API */ async setSaturationRelative(saturation) { - try { - const response = await this.setSaturation({value: saturation, mode: 'enabled', type: 'R'}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setSaturation({value: saturation, mode: 'enabled', type: 'R'}); } /** @@ -915,12 +726,7 @@ class Twinkly { * @see REST-API */ async setSaturationDisabled() { - try { - const response = await this.setSaturation({value: 0, mode: 'disabled', type: 'A'}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setSaturation({value: 0, mode: 'disabled', type: 'A'}); } /** @@ -930,12 +736,7 @@ class Twinkly { * @see REST-API */ async setSaturation(data) { - try { - const response = await this._post('led/out/saturation', data); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/out/saturation', data); } /** @@ -944,12 +745,7 @@ class Twinkly { * @see REST-API */ async resetLED() { - try { - const response = await this._get('reset'); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('reset'); } /** @@ -958,12 +754,7 @@ class Twinkly { * @see REST-API */ async reset2LED() { - try { - const response = await this._get('reset2'); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('reset2'); } /** @@ -971,37 +762,38 @@ class Twinkly { * @param {{r: Number, g: Number, b: Number}[]} frame * @return {Promise<{code: Number}>} * @see REST-API + * @see Code xled */ async sendRealtimeFrame(frame) { - try { - const response = await this._post('led/led/rt/frame', frame, {'Content-Type': 'application/octet-stream'}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('led/rt/frame', frame, {'Content-Type': 'application/octet-stream'}); + } + + /** + * Send Realtime Frame Socket + * @return {Promise<{code}>} + * @see Realtime + * @see Code xled + * @see Code ttls + */ + async sendRealtimeFrameSocket() { + // https://github.com/jschlyter/ttls/blob/main/ttls/client.py#L309-L318 + // const socket = dgram.createSocket('udp4'); + // socket.send(Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 0, 8, REALTIME_UDP_PORT_NUMBER, this.host); } /** * Get firmware version - * @return {Promise<{firmware: {version: String}, code: Number}>} + * @return {Promise<{version: String, code: Number}>} * @see REST-API */ async getFirmwareVersion() { - try { - const response = await this._handleHttpTwinklyCodeCheck('GET', 'fw/version', null); + const response = await this._sendRequestTwinklyCodeCheck('GET', 'fw/version', null); - /** @type {{version: String}} */ - const firmware = {}; - this._cloneTwinklyResponse(response, firmware); + const oldFirmware = this.firmware; + this.firmware = response.version; + await this._checkDataChange('firmware', this.firmware, oldFirmware); - const oldFirmware = this.firmware; - this.firmware = firmware.version; - await this._checkDataChange('firmware', this.firmware, oldFirmware); - - return {firmware: firmware, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return response; } isFirmwareEmpty() { @@ -1010,29 +802,17 @@ class Twinkly { /** * Get Status - * @return {Promise<{status: {status: Number}, code: Number}>} + * @return {Promise<{status: Number, code: Number}>} * @see REST-API */ async getStatus() { - try { - const response = await this._get('status'); - - /** - * @type {{status: Number}} - */ - const status = {}; - this._cloneTwinklyResponse(response, status); - - return {status: status, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('status'); } /** * Get list of movies - * @return {Promise<{movies: {movies: {id: Number, unique_id: String, name: String, descriptor_type: String, leds_per_frame: Number, frames_number: Number, fps: Number}[], - * available_frames: Number, max_capacity: Number}, code: Number}>} + * @return {Promise<{movies: {id: Number, unique_id: String, name: String, descriptor_type: String, leds_per_frame: Number, frames_number: Number, fps: Number}[], + * available_frames: Number, max_capacity: Number, code: Number}>} * @see REST-API */ async getListOfMovies() { @@ -1041,26 +821,19 @@ class Twinkly { try { const response = await this._get('movies'); - /** - * @type {{movies: {id: Number, unique_id: String, name: String, descriptor_type: String, leds_per_frame: Number, frames_number: Number, fps: Number}[], - * available_frames: Number, max_capacity: Number}} - */ - const movies = {}; - this._cloneTwinklyResponse(response, movies); - // Liste füllen - if (movies.movies) { - for (const movie of movies.movies) { + if (response.movies) { + for (const movie of response.movies) { this.ledMovies[movie.id] = movie.name; } } - return {movies: movies, code: response['code']}; + return response; } catch (e) { // Wenn kein Movie aktiv ist, kommt hier eine Exception // HTTP Error (204) No Content: {"code":1102} if (e.message && (e.message.includes('ECONNRESET') || e.message.includes('(204)'))) { - return {movies: {movies: [], available_frames: 0, max_capacity: 0}, code: HTTPCodes.values.ok}; + return {movies: [], available_frames: 0, max_capacity: 0, code: HTTPCodes.values.ok}; } throw Error(e.message); @@ -1075,25 +848,17 @@ class Twinkly { /** * Get current movie - * @return {Promise<{movie: {id: Number, unique_id: String, name: String}, code: Number}>} + * @return {Promise<{id: Number, unique_id: String, name: String, code: Number}>} * @see REST-API */ async getCurrentMovie() { try { - const response = await this._get('movies/current'); - - /** - * @type {{id: Number, unique_id: String, name: String}} - */ - const movie = {}; - this._cloneTwinklyResponse(response, movie); - - return {movie: movie, code: response['code']}; + return await this._get('movies/current'); } catch (e) { // Wenn kein Movie aktiv ist, kommt hier eine Exception // HTTP Error (204) No Content: {"code":1102} if (e.message && (e.message.includes('ECONNRESET') || e.message.includes('(204)'))) { - return {movie: {id: -1, unique_id: '', name: ''}, code: HTTPCodes.values.ok}; + return {id: -1, unique_id: '', name: '', code: HTTPCodes.values.ok}; } throw Error(e.message); @@ -1107,12 +872,7 @@ class Twinkly { * @see REST-API */ async setCurrentMovie(movieId) { - try { - const response = await this._post('movies/current', {id: movieId}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('movies/current', {id: movieId}); } // Initiate WiFi network scan @@ -1121,28 +881,14 @@ class Twinkly { /** * Get network status - * @return {Promise<{status: {mode: Number, - * station: {ip: String, gw: String, mask: String, rssi: Number, ssid: String, status: String}, - * ap: {enc: Number, ip: String, channel: Number, max_connections: Number, password_changed: Number, ssid: String, ssid_hidden: Number}}, + * @return {Promise<{mode: Number, + * station: {ip: String, gw: String, mask: String, rssi: Number, ssid: String, status: String}, + * ap: {enc: Number, ip: String, channel: Number, max_connections: Number, password_changed: Number, ssid: String, ssid_hidden: Number}, * code: Number}>} * @see REST-API */ async getNetworkStatus() { - try { - const response = await this._get('network/status'); - - /** - * @type {{mode: Number, - * station: {ip: String, gw: String, mask: String, rssi: Number, ssid: String, status: String}, - * ap: {enc: Number, ip: String, channel: Number, max_connections: Number, password_changed: Number, ssid: String, ssid_hidden: Number}}} - */ - const status = {}; - this._cloneTwinklyResponse(response, status); - - return {status: status, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('network/status'); } /** @@ -1150,12 +896,7 @@ class Twinkly { * @see REST-API */ async setNetworkStatusAP(data) { - try { - const response = await this.setNetworkStatus({mode: 2, ap: data}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setNetworkStatus({mode: 2, ap: data}); } /** @@ -1163,12 +904,7 @@ class Twinkly { * @see REST-API */ async setNetworkStatusStation(data) { - try { - const response = await this.setNetworkStatus({mode: 1, station: data}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this.setNetworkStatus({mode: 1, station: data}); } /** @@ -1178,37 +914,16 @@ class Twinkly { * @see REST-API */ async setNetworkStatus(data) { - try { - const response = await this._post('network/status', data); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('network/status', data); } /** * Get MQTT configuration - * @return {Promise<{mqtt: {broker_host : String, - * broker_port : Number, - * client_id : String, - * user : String, - * keep_alive_interval: Number}, code: Number}>} + * @return {Promise<{broker_host : String, broker_port : Number, client_id : String, user : String, keep_alive_interval: Number, code: Number}>} * @see REST-API */ async getMqttConfiguration() { - try { - const response = await this._get('mqtt/config'); - - /** - * @type {{broker_host : String, broker_port : Number, client_id : String, user: String, keep_alive_interval: Number}} - */ - const mqtt = {}; - this._cloneTwinklyResponse(response, mqtt); - - return {mqtt: mqtt, code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._get('mqtt/config'); } /** @@ -1218,43 +933,27 @@ class Twinkly { * @see REST-API */ async setMqttConfiguration(data) { - try { - data = typeof data === 'string' ? JSON.parse(data) : data; - - const response = await this._post('mqtt/config', data); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('mqtt/config', typeof data === 'string' ? JSON.parse(data) : data); } /** * Get playlist - * @return {Promise<{playlist: {unique_id: String, name: String, entries: {id: Number, handle: Number, name: String, unique_id: String, duration: Number}[]}, code: Number}>} + * @return {Promise<{unique_id: String, name: String, entries: {id: Number, handle: Number, name: String, unique_id: String, duration: Number}[], code: Number}>} * @see REST-API */ async getPlaylist() { this.playlist = {}; - try { - const response = await this._get('playlist'); - /** - * @type {{unique_id: String, name: String, entries:{id: Number, handle: Number, name: String, unique_id: String, duration: Number}[]}} - */ - const playlist = {}; - this._cloneTwinklyResponse(response, playlist); + const response = await this._get('playlist'); - // Liste füllen - if (playlist.entries) { - for (const entry of playlist.entries) { - this.playlist[entry.id] = entry.name; - } + // Liste füllen + if (response.entries) { + for (const entry of response.entries) { + this.playlist[entry.id] = entry.name; } - - return {playlist: playlist, code: response['code']}; - } catch (e) { - throw Error(e.message); } + + return response; } /** @@ -1264,12 +963,7 @@ class Twinkly { * @see REST-API */ async createPlaylist(entries) { - try { - const response = await this._post('playlist', entries); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._post('playlist', entries); } /** @@ -1278,35 +972,22 @@ class Twinkly { * @see REST-API */ async deletePlaylist() { - try { - const response = await this._delete('playlist'); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); - } + return await this._delete('playlist'); } /** * Get current playlist entry - * @return {Promise<{playlist : {id: Number, unique_id: String, name: String, duration: Number}, code: Number}>} + * @return {Promise<{id: Number, unique_id: String, name: String, duration: Number, code: Number}>} * @see REST-API */ async getCurrentPlaylistEntry() { try { - const response = await this._get('playlist/current'); - - /** - * @type {{id: Number, unique_id: String, name: String, duration: Number}} - */ - const playlist = {}; - this._cloneTwinklyResponse(response, playlist); - - return {playlist : playlist , code: response['code']}; + return await this._get('playlist/current'); } catch (e) { // Wenn keine Playlist existiert, kommt hier eine Exception // HTTP Error (204) No Content: {"code":1102} if (e.message && (e.message.includes('ECONNRESET') || e.message.includes('(204)'))) { - return {playlist: {id: -1, unique_id: '', name: '', duration: 0}, code: HTTPCodes.values.ok}; + return {id: -1, unique_id: '', name: '', duration: 0, code: HTTPCodes.values.ok}; } throw Error(e.message); @@ -1320,31 +1001,150 @@ class Twinkly { * @see REST-API */ async setCurrentPlaylistEntry(playlistId) { - try { - const response = await this._post('playlist/current', {id: playlistId}); - return {code: response['code']}; - } catch (e) { - throw Error(e.message); + return await this._post('playlist/current', {id: playlistId}); + } + + /** + * Set mic configuration + * @return {Promise<{filters: {}, silence_threshold: 0, active_range: number, brightness_depth: number, + * hue_depth: number, value: depth, saturation_depth: number, code: number}>} + * @see REST-API + */ + async getMicConfig() { + return await this._get('mic/config'); + } + + /** + * Get mic sample + * @return {Promise<{sampled_value: number, code: number}>} + * @see REST-API + */ + async getMicSample() { + return await this._get('mic/sample'); + } + + /** + * Get summary + * @return {Promise<{code: number}>} + * @see REST-API + */ + async getSummary() { + return await this._get('summary'); + } + + /** + * Get music drivers + * @return {Promise<{drivers_number: Number, unique_ids: String[], code: number}>} + * @see REST-API + */ + async getMusicDrivers() { + return await this._get('music/drivers'); + } + + /** + * Get current music driver + * @return {Promise<{handle: Number, code: number}>} + */ + async getCurrentMusicDriver() { + // TODO: Inhalte prüfen + return await this._get('music/drivers/current'); + } + + /** + * Set current music driver + * @param {string} driverName + * @return {Promise<{code: number}>} + */ + async setCurrentMusicDriver(driverName) { + const uniqueId = this._getMusicDriverIdByName(driverName); + if (uniqueId === '') { + throw Error(`${driverName} is an invalid music driver!`); + } + + const currentDriver = await this.getCurrentMusicDriver(); + if (currentDriver.handle === -1) { + await this.setNextMusicDriver(); } + + return await this._post('music/drivers/current', {unique_id: uniqueId}); } - // Get mic config + /** + * Get Music driver by name + * @param {string} name + * @return {string} + * @private + */ + _getMusicDriverIdByName(name) { + if (Object.keys(TWINKLY_MUSIC_DRIVERS_OFFICIAL).includes(name)) { + return TWINKLY_MUSIC_DRIVERS_OFFICIAL[name]; + } else if (Object.keys(TWINKLY_MUSIC_DRIVERS_UNOFFICIAL).includes(name)) { + this.adapter.log.info(`Music driver ${name} is defined, but is not officially supported`); + return TWINKLY_MUSIC_DRIVERS_UNOFFICIAL[name]; + } else { + return ''; + } + } + + /** + * Set next music driver + * @return {Promise<{code: number}>} + */ + async setNextMusicDriver() { + return await this._post('music/drivers/current', {action: 'next'}); + } - // Get mic sample + /** + * Set previous music driver + * @return {Promise<{code: number}>} + */ + async setPreviousMusicDriver() { + return await this._post('music/drivers/current', {action: 'prev'}); + } - // Get summary + /** + * Get music driver set + * @return {Promise<{current: Number, count: number, driversets: {id: number, count: number, unique_ids: string}[], code: number}>} + * @see REST-API + */ + async getMusicDriversSets() { + return await this._get('music/drivers/sets'); + } - // Get music drivers + /** + * Get current music driver set + * @return {Promise<{driverset_id: Number, code: number}>} + * @see REST-API + */ + async getCurrentMusicDriversSet() { + return await this._get('music/drivers/sets/current'); + } - // Get music drivers sets + /** + * Enable music + * @param {boolean} enable + * @return {Promise<{code: number}>} + */ + async setMusicOn(enable) { + return await this._post('music/enabled', {enabled: enable ? 1 : 0}); + } - // Get current music driverset + /** + * Set Token + * @param token + * @private + */ + _setToken(token) { + this.token = token; + this.axiosInstance.defaults.headers.common['X-Auth-Token'] = token; + } /** * Reset Token Data + * @private */ resetToken() { - this.token = ''; + this._setToken(''); this.expires = 0; this.challengeResponse = ''; } @@ -1352,6 +1152,7 @@ class Twinkly { /** * Check if Token empty * @return {boolean} + * @private */ isTokenEmpty() { return this.token === ''; @@ -1405,76 +1206,69 @@ class Twinkly { /** * @param {string} path - * @param {{}} headers - * @return {Promise<{}>} + * @return {Promise} * @private */ - async _get(path, headers = {}) { - return this._handleHttp('GET', path, null, headers); + async _get(path) { + return this._sendRequestTwinkly('GET', path, null); } /** * @param {string} path * @param {any} data * @param {{}} headers - * @return {Promise<{}>} + * @return {Promise} * @private */ async _post(path, data, headers = {}) { - return this._handleHttp('POST', path, data, headers); + return this._sendRequestTwinkly('POST', path, data, headers); } /** * @param {string} path * @param {{}} headers - * @return {Promise<{}>} + * @return {Promise} * @private */ async _delete(path, headers = {}) { - return this._handleHttp('DELETE', path, null, headers); + return this._sendRequestTwinkly('DELETE', path, null, headers); } /** - * @param {'GET'|'POST'|'DELETE'} mode + * @param {axios.Method} mode * @param {string} path * @param {any} data * @param {{}} headers - * @return {Promise<{}>} + * @return {Promise} * @private */ - async _handleHttp(mode, path, data, headers = {}) { - try { - await this.ensure_token(false); - } catch (e) { - throw Error(e.message); - } + async _sendRequestTwinkly(mode, path, data, headers = {}) { + await this.ensure_token(false); if (this.isTokenEmpty()) return {code: HTTPCodes.values.errorLogin}; try { // Ausführen... - return await this._handleHttpTwinklyCodeCheck(mode, path, data, headers); + return await this._sendRequestTwinklyCodeCheck(mode, path, data, headers); } catch (e) { // Bei "Invalid Token" wird es erneut versucht! - if (e.message !== HTTPCodes.INVALID_TOKEN) - throw Error(e.message); - - try { - // Token erneuern - await this.ensure_token(true); - } catch (e) { + if (e.message !== HTTPCodes.INVALID_TOKEN) { throw Error(e.message); } + // Token erneuern + await this.ensure_token(true); + if (this.isTokenEmpty()) return {code: HTTPCodes.values.errorLogin}; try { // Erneut ausführen... - return await this._handleHttpTwinklyCodeCheck(mode, path, data, headers); + return await this._sendRequestTwinklyCodeCheck(mode, path, data, headers); } catch (e) { // Wenn wieder fehlerhaft, dann Pech gehabt. Token wird gelöscht... - if (e.message === HTTPCodes.INVALID_TOKEN) + if (e.message === HTTPCodes.INVALID_TOKEN) { this.resetToken(); + } throw Error(e.message); } @@ -1482,28 +1276,21 @@ class Twinkly { } /** - * POST and check Twinkly code + * Send Request and check Twinkly code * - * @param {'GET'|'POST'|'DELETE'} mode + * @param {axios.Method} mode * @param {string} path * @param {any} data * @param {{}} headers - * @return {Promise<{}>} + * @return {Promise} * @private */ - async _handleHttpTwinklyCodeCheck(mode, path, data, headers= {}) { - const httpHeaders = {}; - this._addHttpHeaders(headers, httpHeaders); - + async _sendRequestTwinklyCodeCheck(mode, path, data, headers= {}) { if (!this.connected) return {code: HTTPCodes.values.errorConnect}; let response; try { - switch (mode) { - case 'GET' : response = await request.getRequest(this.adapter, this.base() + '/' + path, httpHeaders); break; - case 'POST' : response = await request.postRequest(this.adapter, this.base() + '/' + path, data, httpHeaders); break; - case 'DELETE': response = await request.deleteRequest(this.adapter, this.base() + '/' + path, httpHeaders); break; - } + response = await this._sendRequest(path, data, mode, headers); } catch (e) { if (e.message && String(e.message).includes(HTTPCodes.INVALID_TOKEN)) { throw Error(HTTPCodes.INVALID_TOKEN); @@ -1520,14 +1307,14 @@ class Twinkly { /** * Check Twinkly code in response * - * @param {'GET'|'POST'|'DELETE'} mode + * @param {axios.Method} mode * @param {String} path * @param {any} response * @private */ _checkResponseTwinklyCode(mode, path, response) { if (response && typeof response === 'object') { - const code = response['code']; + const code = response.code; if (code && code !== HTTPCodes.values.ok) { throw Error(`[${mode}.${path}] (${code}) ${HTTPCodes.text[code]}`); } @@ -1535,66 +1322,82 @@ class Twinkly { } /** - * Add default Headers - * - * @param {{}} param - * @param {{}} send - * @private + * Ping + * @param {boolean} usePing + * @returns {Promise} */ - _addHttpHeaders(param, send) { - for (const header of Object.keys(param)) { - if (!Object.keys(send).includes(header)) { - send[header] = param[header]; + async checkConnection(usePing) { + let result = false; + + if (usePing) { + try { + /** @type {{host: string, alive: boolean, ms: number}} */ + const response = await ping.probe(this.host, {log: this.adapter.log.debug}); + this.adapter.log.debug(`[${this.name}.ping] Ping result for ${response.host}: ${response.alive} in ${response.ms === null ? '-' : response.ms}ms`); + result = response.alive; + } catch (e) { + this.adapter.log.error(`[${this.name}.ping]: ${e}`); + } + } else { + try { + const response = await this.getDeviceDetails(); + result = response.code === HTTPCodes.values.ok; + } catch (e) { + // } } - // Add token if available - if (!this.isTokenEmpty()) { - send['X-Auth-Token'] = this.token; - } + this.connected = result; + return result; } /** - * Add default Headers - * - * @param {{}} input - * @param {{}} output + * @param {string} url + * @param {any} body + * @param {axios.Method} method + * @param {{}} headers + * @return {Promise} * @private */ - _cloneTwinklyResponse(input, output) { - tools.cloneObject(input, output); - if (Object.keys(output).includes('code')) { - delete output['code']; - } - } + async _sendRequest(url, body, method, headers = {}) { + this.adapter.log.debug(`[${this.name}._sendRequest.${method}] url="${url}", body="${body !== undefined ? JSON.stringify(body) : 'null'}", headers="${JSON.stringify(headers)}"`); - /** - * Ping - * @param {boolean} usePing - * @returns {Promise} - */ - async checkConnection(usePing) { - let result = false; + let response; try { - if (usePing) { - await ping.probe(this.host, {log: this.adapter.log.debug}) - .then(({host, alive, ms}) => { - this.adapter.log.debug(`[${this.name}.ping] Ping result for ${host}: ${alive} in ${ms === null ? '-' : ms}ms`); - result = alive; - }) - .catch(error => { - this.adapter.log.error(`[${this.name}.ping]: ${error}`); - }); + response = await this.axiosInstance.request({url: url, method: method, headers: headers, data: body}); + } catch (/** @type {axios.AxiosError | Error} */ e) { + // Logging + let message; + if (e instanceof axios.AxiosError) { + /** @type {object} */ + const json = e.toJSON(); + message = `code="${e.code}", status=${json.status}, config="${JSON.stringify(json.config)}", stack="${json.stack}"`; } else { - const response = await this.getDeviceDetails(); - result = response.code === HTTPCodes.values.ok; + message = `stack="${e.stack}"`; + } + + this.adapter.log.debug(`[${this.name}._sendRequest.${method}] ${e.name}: "${e.message}", ${message}`); + + // Error handling + if (e instanceof axios.AxiosError && e.response && e.response.status !== 200) { + throw Error(`HTTP Error (${e.response.status}) ${e.response.statusText}${e.response.data ? ': ' + JSON.stringify(e.response.data) : ''}`); + } else { + throw Error(e.message ? e.message : e); } - } catch (e) { - result = false; } - this.connected = result; - return result; + if (typeof response === 'undefined') { + throw Error('No response received'); + } + + if (response.status !== 200) { + throw Error(`HTTP Error (${response.status}) ${response.statusText}${response.data ? ': ' + JSON.stringify(response.data) : ''}`); + } else { + if (response.data) { + this.adapter.log.debug(`[${this.name}._sendRequest.${method}.response] ${JSON.stringify(response.data)}`); + } + return response.data; + } } } diff --git a/main.js b/main.js index 33378f74..07a7ff71 100644 --- a/main.js +++ b/main.js @@ -476,8 +476,8 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getDeviceDetails(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.details, apiObjectsMap.details); - await saveJSONinState(connectionName, connectionName, response.details, apiObjectsMap.details); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.details); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.details); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.details.parent.id} ${e}`); @@ -490,8 +490,8 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getFirmwareVersion(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.firmware, apiObjectsMap.firmware); - await saveJSONinState(connectionName, connectionName, response.firmware, apiObjectsMap.firmware); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.firmware); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.firmware); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.firmware.parent.id} ${e}`); @@ -504,10 +504,10 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getBrightness(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.bri, apiObjectsMap.ledBri); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledBri); try { await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.ledBri.child.value.id, - response.bri.mode !== 'disabled' ? response.bri.value : -1, true); + response.mode !== 'disabled' ? response.value : -1, true); } catch (e) { // } @@ -523,13 +523,13 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getLEDColor(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.color, apiObjectsMap.ledColor); - await saveJSONinState(connectionName, connectionName, response.color, apiObjectsMap.ledColor); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledColor); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.ledColor); try { // Hex Version await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.ledColor.parent.id + '.' + apiObjectsMap.ledColor.child.hex.id, - tools.rgbToHex(response.color.red, response.color.green, response.color.blue, false), true); + tools.rgbToHex(response.red, response.green, response.blue, false), true); } catch (e) { // } @@ -545,9 +545,9 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getLEDConfig(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.config, apiObjectsMap.ledConfig); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledConfig); try { - await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.ledConfig.id, JSON.stringify(response.config.strings), true); + await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.ledConfig.id, JSON.stringify(response.strings), true); } catch (e) { // } @@ -563,8 +563,8 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getCurrentLEDEffect(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.effect, apiObjectsMap.ledEffect); - await saveJSONinState(connectionName, connectionName, response.effect, apiObjectsMap.ledEffect); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledEffect); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.ledEffect); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.ledEffect.parent.id} ${e}`); @@ -577,8 +577,8 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getLayout(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.layout, apiObjectsMap.ledLayout); - await saveJSONinState(connectionName, connectionName, response.layout, apiObjectsMap.ledLayout); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledLayout); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.ledLayout); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.ledLayout.parent.id} ${e}`); @@ -591,10 +591,10 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getLEDMode(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.mode, apiObjectsMap.ledMode); - await saveJSONinState(connectionName, connectionName, response.mode, apiObjectsMap.ledMode); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledMode); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.ledMode); try { - await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.on.id, response.mode.mode !== twinkly.lightModes.value.off, true); + await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.on.id, response.mode !== twinkly.lightModes.value.off, true); } catch (e) { // } @@ -613,8 +613,8 @@ async function poll(specificConnection = '', filter = []) { // ... then get current Movie const response = await connection.twinkly.getCurrentMovie(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.movie, apiObjectsMap.ledMovie); - await saveJSONinState(connectionName, connectionName, response.movie, apiObjectsMap.ledMovie); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledMovie); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.ledMovie); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.ledMovie.parent.id} ${e}`); @@ -630,8 +630,8 @@ async function poll(specificConnection = '', filter = []) { // ... then get current Playlist Entry const response = await connection.twinkly.getCurrentPlaylistEntry(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.playlist, apiObjectsMap.ledPlaylist); - await saveJSONinState(connectionName, connectionName, response.playlist, apiObjectsMap.ledPlaylist); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledPlaylist); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.ledPlaylist); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.ledPlaylist.parent.id} ${e}`); @@ -644,10 +644,10 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getSaturation(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.sat, apiObjectsMap.ledSat); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.ledSat); try { await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.ledSat.child.value.id, - response.sat.mode !== 'disabled' ? response.sat.value : -1, true); + response.mode !== 'disabled' ? response.value : -1, true); } catch (e) { // } @@ -663,8 +663,8 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getMqttConfiguration(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.mqtt, apiObjectsMap.mqtt); - await saveJSONinState(connectionName, connectionName, response.mqtt, apiObjectsMap.mqtt); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.mqtt); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.mqtt); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.mqtt.parent.id} ${e}`); @@ -677,8 +677,8 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getDeviceName(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.name, apiObjectsMap.name); - await saveJSONinState(connectionName, connectionName, response.name, apiObjectsMap.name); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.name); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.name); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.name.parent.id} ${e}`); @@ -691,8 +691,8 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getNetworkStatus(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.status, apiObjectsMap.networkStatus); - await saveJSONinState(connectionName, connectionName, response.status, apiObjectsMap.networkStatus); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.networkStatus); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.networkStatus); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.networkStatus.parent.id} ${e}`); @@ -717,8 +717,8 @@ async function poll(specificConnection = '', filter = []) { try { const response = await connection.twinkly.getTimer(); if (response.code === twinkly.HTTPCodes.values.ok) { - await checkTwinklyResponse(connectionName, response.timer, apiObjectsMap.timer); - await saveJSONinState(connectionName, connectionName, response.timer, apiObjectsMap.timer); + await checkTwinklyResponse(connectionName, response, apiObjectsMap.timer); + await saveJSONinState(connectionName, connectionName, response, apiObjectsMap.timer); } } catch (e) { adapter.log.error(`Could not get ${connectionName}.${apiObjectsMap.timer.parent.id} ${e}`); @@ -1425,7 +1425,7 @@ async function checkTwinklyResponse(connectionName, response, mapping) { const name = mapping.parent ? mapping.parent.id : mapping.id; // check newSince - await checkTwinklyResponseNewSince(connectionName, name, response, mapping); + await checkTwinklyResponseNewSince(connectionName, name, response, mapping, true); // check deprecated await checkTwinklyResponseDeprecated(connectionName, name, response, mapping); } catch (e) { @@ -1441,8 +1441,9 @@ async function checkTwinklyResponse(connectionName, response, mapping) { * @param {{id: string, name: string, hide?: boolean} | * {parent: {id: string, name: string, hide?: boolean}, * child: {}, expandJSON: boolean, logItem?: boolean, hide?: boolean}} mapping + * @param {boolean} root */ -async function checkTwinklyResponseNewSince(connectionName, name, response, mapping) { +async function checkTwinklyResponseNewSince(connectionName, name, response, mapping, root) { if (typeof response === 'undefined' || typeof response !== 'object') return; for (const key of Object.keys(response)) { @@ -1456,13 +1457,14 @@ async function checkTwinklyResponseNewSince(connectionName, name, response, mapp } if (continueCheck) { - await checkTwinklyResponseNewSince(connectionName, name + '.' + key, response[key], mapping.child[key]); + await checkTwinklyResponseNewSince(connectionName, name + '.' + key, response[key], mapping.child[key], false); } else { await handleSentryMessage(connectionName, 'checkTwinklyResponse', 'reintroduced', `${connectionName}:${name}:${key}`, `Item reintroduced: ${connectionName}.${name}.${key}`, 'warning', {[typeof response[key]]: response[key]}, 'query'); } } - } else { + // Im Root der Response liegt die Property "code", die ignoriert werden kann + } else if (!root || key !== 'code') { await handleSentryMessage(connectionName, 'checkTwinklyResponse', 'newSince', `${connectionName}:${name}:${key}`, `New Item detected: ${connectionName}.${name}.${key}`, 'warning', {[typeof response[key]]: response[key]}, 'query'); } @@ -1622,7 +1624,7 @@ async function updateMovies(connectionName) { const response = await connection.twinkly.getListOfMovies(); if (statesConfig.includes(apiObjectsMap.ledMovies.id)) { try { - await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.ledMovies.id, JSON.stringify(response.movies.movies), true); + await adapter.setStateAsync(connectionName + '.' + apiObjectsMap.ledMovies.id, JSON.stringify(response.movies), true); } catch (e) { // } diff --git a/tsconfig.json b/tsconfig.json index 7bff724c..eea6d110 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,8 +23,7 @@ // "noUnusedLocals": true, // "noUnusedParameters": true, - // Consider targetting es2017 or higher if you require the new NodeJS 8+ features - "target": "es2015", + "target": "es2022", }, "include": [