From 3af0bef05fb59bc4145b8a4fcd1bfa5ee13635e0 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Fri, 6 Sep 2024 11:50:54 +0800 Subject: [PATCH] * (bluefox) Corrected error with array comparison * (bluefox) added tests --- README.md | 3 + lib/devices.js | 66 +++++++++-------- lib/dontBeSoSoef.js | 13 ++++ main.js | 73 ++++++++++++------- package.json | 2 + test/legacy.js | 142 ++++++++++++++++++++++++++++++++++++ test/package.js | 2 +- test/simulate.js | 174 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 418 insertions(+), 57 deletions(-) create mode 100644 test/legacy.js create mode 100644 test/simulate.js diff --git a/README.md b/README.md index fbb484b..390aeca 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ For example, `red = 0`, blue and green will stay unchanged. Placeholder for the next version (at the beginning of the line): ### **WORK IN PROGRESS** --> +### **WORK IN PROGRESS** +* (bluefox) Corrected error with array comparison + ### 2.0.0 (2024-09-05) * (bluefox) The adapter was completely refactored * (bluefox) Added compact mode diff --git a/lib/devices.js b/lib/devices.js index b8fa32e..65ad39e 100644 --- a/lib/devices.js +++ b/lib/devices.js @@ -52,12 +52,12 @@ const LW12 = { responseLen2: 11, on: [0xCC, 0x23, 0x33], off: [0xCC, 0x24, 0x33], - rgb: [0x56, VARS.red, VARS.green, VARS.blue, 0xAA], progOn: [0xCC, 0x21, 0x33], progOff: [0xCC, 0x20, 0x33], - progNo: [0xBB, VARS.prog, VARS.speed, 0x44], statusRequest: [0xEF, 0x01, 0x77], - programNames: programNames, + programNames, + progNo: [0xBB, VARS.prog, VARS.speed, 0x44], + rgb: [0x56, VARS.red, VARS.green, VARS.blue, 0xAA], decodeResponse: data => { if (data[0] !== 0x66 || data[1] !== 0x01) { return [11, null]; @@ -89,15 +89,15 @@ const LD382A = { responseLen2: 4, on: [0x71, 0x23, 0x0f], off: [0x71, 0x24, 0x0f], - rgb: [0x31, VARS.red, VARS.green, VARS.blue, 0xff /*VARS.white*/, 0x00, 0x0f], - rgbw: [0x31, VARS.red, VARS.green, VARS.blue, VARS.white, 0x00, 0x0f], progOn: [0x71, 0x21, 0x0f], progOff: [0x71, 0x20, 0x0f], - progNo: [97, VARS.prog, VARS.speed, 0x0f], statusRequest: [0x81, 0x8A, 0x8B], - programNames: programNames, + programNames, + progNo: [97, VARS.prog, VARS.speed, 0x0f], + rgbw: [0x31, VARS.red, VARS.green, VARS.blue, VARS.white, 0x00, 0x0f], + rgb: [0x31, VARS.red, VARS.green, VARS.blue, 0xff /*VARS.white*/, 0x00, 0x0f], - decodeResponse: function(data) { + decodeResponse: data => { // If power on / off request, the result is 4 bytes long and the second byte is 0x71 if (data[1] === 0x71) { return [4, null]; @@ -106,20 +106,23 @@ const LD382A = { return [0, null]; } //[129, 4, 35, 97, 33, 9, 11, 22, 33, 255, 3, 0, 0, 119] - return [14, { - //power: ((data[2] === 0x23) ? true : false), - on: (data[2] === 0x23), - //power: ((data[13] & 0x01) ? true : false), - //power: ((data[13] & 0x01) ? false : true), - progNo: data[3],//mode - progOn: data[4] === 33, //modeRun - progSpeed: data[5], //modeSpeed - red: data[6], - green: data[7], - blue: data[8], - white: data[9], - }]; - } + return [ + 14, + { + // power: ((data[2] === 0x23) ? true : false), + on: data[2] === 0x23, + // power: ((data[13] & 0x01) ? true : false), + // power: ((data[13] & 0x01) ? false : true), + progNo: data[3],// mode + progOn: data[4] === 33, // modeRun + progSpeed: data[5], // modeSpeed + red: data[6], + green: data[7], + blue: data[8], + white: data[9], + }, + ]; + }, }; const LD686 = { @@ -139,9 +142,9 @@ const LD686 = { progOff: [0x71, 0x20, 0x0f], progNo: [97, VARS.prog, VARS.speed, 0x0f], statusRequest: [0x81, 0x8A, 0x8B], - programNames: programNames, + programNames, - decodeResponse: function (data) { + decodeResponse: data => { if (data[0] !== 129) { return [0, null]; } @@ -168,11 +171,11 @@ const LD382 = Object.assign({}, LD382A, { // not tested const UFO = Object.assign({}, LD382A, { // not tested on: [0x71, 0x23], off: [0x71, 0x24], - rgb: [0x31, VARS.red, VARS.green, VARS.blue, 0x00, 0x00, 0x00], - rgbw: [0x31, VARS.red, VARS.green, VARS.blue, VARS.white, 0x00], progOn: [0x71, 0x21], progOff: [0x71, 0x20], progNo: [0x61, VARS.prog, VARS.speed], + rgbw: [0x31, VARS.red, VARS.green, VARS.blue, VARS.white, 0x00], + rgb: [0x31, VARS.red, VARS.green, VARS.blue, 0x00, 0x00, 0x00], statusRequest: [0x81, 0x8A, 0x8B], }); @@ -188,6 +191,7 @@ const MiLight = { colorSteps: 255, delay: 100, + // this function cannot be lambda (=>) because it is used with "this" setZone: function (zone) { const self = this; if (zone > 4) { @@ -255,12 +259,10 @@ const MiLight = { wtmaxBright: [0xB5, 0x00, 0x55], wtBrightUp: [0x3C, 0x00, 0x55], wtBrightDown: [0x34, 0x00, 0x55], - wtWarmer: [0x3E, 0x00,0x55], + wtWarmer: [0x3E, 0x00, 0x55], wtCooler: [0x3F, 0x00, 0x55], progNo: [0x4D, 0x00, 0x55], - bri: [0x4E, VARS.bright, 0x55], - hue: [0x40, VARS.red, 0x55], pair: [0x25, 0x00, 0x55], // send 3 x with 1 sec delay unPair: [0x25, 0x00, 0x55], // send 15 x with 200 ms delay @@ -269,6 +271,9 @@ const MiLight = { rgb: [0x00], rgbw: [0x00], + bri: [0x4E, VARS.bright, 0x55], + hue: [0x40, VARS.red, 0x55], + programNames: { 0: '[Off]', 1: 'Regenbogen', @@ -342,8 +347,9 @@ const MiLightW = Object.assign({}, MiLight, { module.exports = { VARS, - MiLightW, knownDeviceNames, + + MiLightW, LW12, LD382A, LD686, diff --git a/lib/dontBeSoSoef.js b/lib/dontBeSoSoef.js index 5c59651..387cb2d 100644 --- a/lib/dontBeSoSoef.js +++ b/lib/dontBeSoSoef.js @@ -613,6 +613,18 @@ function Timer(func, timeout, v1) { } } +function compareArrays(arr1, arr2) { + if (arr1.length !== arr2.length) { + return false; + } + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) { + return false; + } + } + return true; +} + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// async function forEachInSystemObjectView(type, id, callback) { @@ -662,4 +674,5 @@ module.exports = { devices, fullExtend, CDevice, + compareArrays, }; diff --git a/main.js b/main.js index 8fe6247..b0df008 100644 --- a/main.js +++ b/main.js @@ -12,6 +12,7 @@ const { devices, fullExtend, CDevice, + compareArrays, } = require('./lib/dontBeSoSoef'); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -494,7 +495,11 @@ class WifiLight { function doIt() { if (self.queue.length > 0) { - setTimeout(doIt, self.queue.length * 2); + this.doItTimeout && clearTimeout(this.doItTimeout); + this.doItTimeout = setTimeout(() => { + this.doItTimeout = null; + doIt(); + }, self.queue.length * 2); return; } if (++i >= cmds.length) { @@ -532,10 +537,13 @@ class WifiLight { timeout = cb; cb = undefined; } - if (this.client) { - this.destroyClient(); - setTimeout(() => this.start(cb), timeout === undefined ? 5000 : timeout); - } + + this.destroyClient(); + + this.reconnectTimeout = this.reconnectTimeout || setTimeout(() => { + this.reconnectTimeout = null; + this.start(cb); + }, timeout === undefined ? 5000 : timeout); } _write(data, cb) { @@ -559,16 +567,16 @@ class WifiLight { this.log(`onClose ${ts}hasError=${hasError} client=${this.config.ip}:${this.config.port}`); }); this.client.on('error', error => { - const ts = debug ? `(${Math.round((Date.now() - this.ts) / 1000)} sec) ` : ''; - - this.log(`onError: ${ts}${error.code !== undefined ? error.code : ''} ${error.message}`); - - switch (error.code) { //error.code + switch (error.code) { case 'ECONNRESET': case 'ETIMEDOUT': case 'EPIPE': + adapter.log.warn(`[${this.config.ip}] onError: ${error.code} ${error.message} - reconnecting in 5 sec...`); this.reconnect(5000); break; + default: + adapter.log.error(`[${this.config.ip}] onError: ${error.code} ${error.message} - will not be reconnected`); + break; } this.setOnline(false); }); @@ -591,6 +599,34 @@ class WifiLight { clearTimeout(this.updateTimer); this.updateTimer = null; } + + if (this.doItTimeout) { + clearTimeout(this.doItTimeout); + this.doItTimeout = null; + } + + if (this.writeTimeout) { + clearTimeout(this.writeTimeout); + this.writeTimeout = null; + } + + if (this.writeTimer) { + clearTimeout(this.writeTimer); + this.writeTimer = null; + } + + if (this.onTimerObject) { + clearTimeout(this.onTimerObject); + this.onTimerObject = null; + } + + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } + + this.prgTimer.clear(); + if (this.client) { this.client.destroy(); this.client = null; @@ -630,21 +666,6 @@ class WifiLight { close() { this.clearQueue(); this.destroyClient(); - if (this.writeTimeout) { - clearTimeout(this.writeTimeout); - this.writeTimeout = null; - } - - if (this.writeTimer) { - clearTimeout(this.writeTimer); - this.writeTimer = null; - } - - if (this.onTimerObject) { - clearTimeout(this.onTimerObject); - this.onTimerObject = null; - } - this.prgTimer.clear(); } runUpdateTimer() { @@ -781,7 +802,7 @@ class WifiLight { if (!(akt.inProcess || (!akt.ctrl && akt.ts && akt.ts < Date.now()))) { break; } - if (this.queue.length <= 1 && !akt.cmd.eq(this.cmds.statusRequest)) { + if (this.queue.length <= 1 && !compareArrays(akt.cmd, this.cmds.statusRequest)) { this.directRefresh(akt.channel); } this.queue.shift(); diff --git a/package.json b/package.json index 8672f0d..ddd64c3 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@alcalzone/release-script-plugin-license": "^3.7.0", "@alcalzone/release-script-plugin-manual-review": "^3.7.0", "@iobroker/adapter-dev": "^1.3.0", + "@iobroker/legacy-testing": "^1.0.13", "@iobroker/testing": "^4.1.3", "@tsconfig/node14": "^14.1.2", "@types/chai": "^4.3.19", @@ -67,6 +68,7 @@ "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"", "test:package": "mocha test/package --exit", "test:integration": "mocha test/integration --exit", + "test:legacy": "mocha test/legacy --exit", "test": "npm run test:js && npm run test:package", "check": "tsc --noEmit -p tsconfig.check.json", "lint": "eslint .", diff --git a/test/legacy.js b/test/legacy.js new file mode 100644 index 0000000..dc7596d --- /dev/null +++ b/test/legacy.js @@ -0,0 +1,142 @@ +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +const expect = require('chai').expect; +const setup = require('@iobroker/legacy-testing'); +const { startServer, stopServer } = require('./simulate'); + +let objects = null; +let states = null; +let onStateChanged = null; + +const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.') + 1); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + console.log(`Try check #${counter}`); + if (counter > 30) { + return cb && cb('Cannot check connection'); + } + + states.getState(`system.adapter.${adapterShortName}.0.alive`, (err, state) => { + if (err) console.error(err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(() => checkConnectionOfAdapter(cb, counter + 1), 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + return cb && cb(`Cannot check value Of State ${id}`); + } + + states.getState(id, (err, state) => { + err && console.error(err); + if (value === null && !state) { + cb && cb(); + } else if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(() => checkValueOfState(id, value, cb, counter + 1), 500); + } + }); +} + +describe(`Test ${adapterShortName} adapter`, function () { + before(`Test ${adapterShortName} adapter: Start js-controller`, function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(async () => { + const config = await setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.devices = [ + { + ip: '127.0.0.1', + port: 5577, + name: 'LD382A', + pollIntervall: 10, + }, + ]; + + await setup.setAdapterConfig(config.common, config.native); + + startServer({ r: 10, g: 20, b: 30, w: 50, progOn: false, on: true }) + .then(() => { + setup.startController( + true, + (/* id, obj */) => {}, + (id, state) => onStateChanged && onStateChanged(id, state), + (_objects, _states) => { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + }); + + it(`Test ${adapterShortName} adapter: Check if adapter started`, function (done) { + this.timeout(60000); + checkConnectionOfAdapter(res => { + res && console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + () => { + states.subscribeMessage('system.adapter.test.0'); + done(); + }); + }); + }); + + it(`Test ${adapterShortName} adapter: Check predefined states`, function (done) { + this.timeout(10000); + checkValueOfState(`${adapterShortName}.0.127_0_0_1.0.r`, 10, () => { + checkValueOfState(`${adapterShortName}.0.127_0_0_1.0.g`, 20, () => { + checkValueOfState(`${adapterShortName}.0.127_0_0_1.0.b`, 30, () => { + checkValueOfState(`${adapterShortName}.0.127_0_0_1.0.w`, 50, () => { + checkValueOfState(`${adapterShortName}.0.127_0_0_1.0.progOn`, false, () => { + checkValueOfState(`${adapterShortName}.0.127_0_0_1.0.on`, true, () => { + done(); + }); + }); + }); + }); + }); + }); + }); + + it(`Test ${adapterShortName} adapter: test control`, function (done) { + this.timeout(10000); + onStateChanged = (id, state) => { + if (id === `${adapterShortName}.0.127_0_0_1.0.r`) { + expect(state.val).to.be.equal(20); + done(); + } + }; + states.setState(`${adapterShortName}.0.127_0_0_1.0.r`, 20, true); + }); + + after(`Test ${adapterShortName} adapter: Stop js-controller`, function (done) { + this.timeout(10000); + + setup.stopController(normalTerminated => { + stopServer() + .then(() => { + console.log(`Adapter normal terminated: ${normalTerminated}`); + done(); + }); + }); + }); +}); diff --git a/test/package.js b/test/package.js index 29e985f..38eacc8 100644 --- a/test/package.js +++ b/test/package.js @@ -2,4 +2,4 @@ const path = require('path'); const { tests } = require('@iobroker/testing'); // Validate the package files -//tests.packageFiles(path.join(__dirname, '..')); +tests.packageFiles(path.join(__dirname, '..')); diff --git a/test/simulate.js b/test/simulate.js new file mode 100644 index 0000000..8a74995 --- /dev/null +++ b/test/simulate.js @@ -0,0 +1,174 @@ +// this is mostly the simulation of LD382A device +const net = require('node:net'); +const { + MiLightW, + LW12, + LD382A, + LD686, + LD382, + UFO, + MiLight, + MiLightRGB, +} = require('../lib/devices'); + +const devices = { + LW12, + LD382A, + LD686, + LD382, + UFO, + MiLightW, + MiLight, + MiLightRGB, +}; + +function string2Buffer(str) { + const arr = str.split(' '); + const buffer = Buffer.alloc(arr.length); + for (let i = 0; i < arr.length; i++) { + buffer[i] = parseInt(arr[i], 16); + } + return buffer; +} + +function findCommand(device, receivedData) { + return Object.keys(device).find(key => { + // compare to arrays + const cmd = device[key]; + if (!Array.isArray(cmd)) { + return false; + } + if (cmd.length !== receivedData.length - 1) { + return false; + } + for (let i = 0; i < cmd.length; i++) { + if (cmd[i] !== receivedData[i]) { + // may be it is a mask + if (i === 1) { + if (cmd[i] < 0) { + return true; + } + } + + return false; + } + } + return true; + }); +} + +const state = { + on: true, + r: 20, + g: 30, + b: 40, + w: 50, + progNo: 1, + speed: 5, + progOn: false, +} + +// Create a TCP server +const server = net.Server(); +server.on('connection', socket => { + console.log('Client connected'); + + socket.on('close', () => { + console.log('Client disconnected'); + }); + + socket.on('data', data => { + console.log(`Client data: ${data.map(byte => byte.toString(16)).join(' ')}`); + // find command + let cmd; + const dev = Object.keys(devices).find(device => { + cmd = findCommand(devices[device], data); + if (cmd) { + console.log(`Found command for ${device}: "${cmd}"`); + return true; + } + }); + if (cmd) { + if (cmd === 'statusRequest') { + setTimeout(() => { + // {"on":true,"progNo":97,"progOn":false,"preogSpeed":10,"red":255,"green":255,"blue":255,"white":0} + const status = [ + 0x81, + 0x33, + state.on ? 0x23 : 0, + state.progNo, + state.progOn ? 33 : 0, + state.speed, + state.r, + state.g, + state.b, + state.w, + 0x06, + 0x00, + 0x00, + 0x46 + ]; + console.log(JSON.stringify(state)); + socket.write(Buffer.from(status)); + }, 50); + } else if (cmd === 'rgbw') { + state.r = data[1]; + state.g = data[2]; + state.b = data[3]; + state.w = data[4]; + } else if (cmd === 'rgb') { + state.r = data[1]; + state.g = data[2]; + state.b = data[3]; + } else if (cmd === 'on') { + state.on = true; + } else if (cmd === 'off') { + state.on = false; + } else if (cmd === 'progNo') { + state.speed = data[2]; + state.progNo = data[1]; + } else if (cmd === 'progOn') { + state.progOn = true; + } else if (cmd === 'progOff') { + state.progOn = false; + } + } + }); +}); + +async function startServer(defaultState) { + return new Promise(resolve => { + if (defaultState) { + Object.assign(state, defaultState); + } + server.listen(5577, '127.0.0.1', () => { + console.log('Server started'); + resolve(); + }); + }); +} + +async function stopServer() { + return new Promise(resolve => { + if (server) { + server?.close(() => { + console.log('Server closed'); + resolve(); + }); + } else { + resolve(); + } + }); +} + +if (module.parent) { + module.exports = { + startServer, + stopServer, + }; +} else { + // or start the instance directly + startServer() + .catch(e => console.error(`Cannot start server: ${e}`)); +} +