UNPKG

iobroker.wifilight

Version:
1,257 lines (1,138 loc) 39.8 kB
const net = require('node:net'); const utils = require(`@iobroker/adapter-core`); const discovery = require('./lib/discovery'); const { rgb2hsv, roundRGB, ct2rgb, hsv2rgb } = require('./lib/colors'); const { Timer, arrayToHex, changeAdapterConfig, clone, delObjectWithStates, devices, fullExtend, CDevice, compareArrays, } = require('./lib/dontBeSoSoef'); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const wifi = {}; const debug = false; const commands = require('./lib/devices'); let adapter = null; function savePrevVersion() { if (!adapter?.ioPack?.common?.version) { return; } const id = `system.adapter.${adapter.namespace}`; const vid = `${id}.prevVersion`; adapter.getForeignObject(vid, (err, obj) => { if (err || !obj) { adapter.setForeignObject( vid, { type: 'state', common: { name: 'version', role: 'indicator.state', desc: 'version check for updates', type: 'string', }, native: {}, }, () => { adapter.setForeignState(vid, { val: adapter.ioPack.common.version, ack: true, from: id }); }, ); } else { adapter.setForeignState(vid, { val: adapter.ioPack.common.version, ack: true, from: id }); } }); } function parseIntVersion(versionString) { if (!versionString) { return 0; } const ar = versionString.toString().split('.'); let iVer = 0; for (let i = 0; i < ar.length; i++) { iVer *= 1000; iVer += ar[i] >> 0; } return iVer; } function checkIfUpdated(callback) { if (!adapter) { if (typeof callback === 'function') { callback(); } return; } const id = `system.adapter.${adapter.namespace}`; const vid = `${id}.prevVersion`; adapter.getForeignState(vid, (err, state) => { const aktVersion = parseIntVersion(adapter.ioPack.common.version); const prevVersion = parseIntVersion(state ? state.val || '0' : '0'); if (prevVersion < aktVersion) { savePrevVersion(); } if (typeof callback === 'function') { callback(); } }); } function fromDeviceName(name) { return commands.knownDeviceNames[name] || commands.knownDeviceNames[name.toUpperCase()]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function onMessage(obj) { if (!obj) { return; } if (obj.command === 'discovery') { discovery.scanForAllDevices( entry => { const ret = !adapter.config.devices.some(e => e.ip === entry.ip); if (ret) { const dev = fromDeviceName(entry.name); entry.type = dev ? dev.type : ''; entry.port = dev?.port ? dev.port : 5577; entry.pollIntervall = 30; } return ret; }, result => { if (obj.callback) { adapter.sendTo(obj.from, obj.command, JSON.stringify(result), obj.callback); } }, ); } else if (obj.command === 'discoveryJson' && obj.callback) { // extract devices let _devices; try { if (typeof obj.message === 'string') { _devices = JSON.parse(obj.message); } else { _devices = obj.message?.devices || []; } } catch { // ignore } _devices = _devices || []; discovery.scanForAllDevices( entry => { const ret = _devices.find(e => e.ip === entry.ip); if (!ret) { const dev = fromDeviceName(entry.name); entry.type = dev ? dev.type : ''; entry.port = dev?.port ? dev.port : 5577; entry.pollIntervall = 30; return true; } // device already exists return false; }, result => { let found = false; result.forEach(e => { if (!_devices.find(d => d.ip === e.ip)) { _devices.push(e); found = true; } }); if (found) { adapter.sendTo(obj.from, obj.command, { native: { devices: _devices } }, obj.callback); } else { adapter.sendTo(obj.from, obj.command, { error: 'Cannot find any device' }, obj.callback); } }, ); } else { adapter.log.warn(`Unknown command: ${obj.command}`); } } function onUnload(callback) { try { Object.keys(wifi).forEach(v => { wifi[v].close(); delete wifi[v]; adapter.log.debug(`unload: ${v}`); }); } catch { // ignore } callback && callback(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const usedStateNames = { online: { n: 'reachable', g: 1, val: false, common: { write: false, type: 'boolean', role: 'indicator.reachable' }, }, on: { n: 'on', g: 3, val: false, common: { type: 'boolean', role: 'switch' } }, bri: { n: 'bri', g: 3, val: 100, common: { type: 'number', min: 0, max: 100, unit: '%', desc: '0..100%', role: 'level.dimmer' }, }, ct: { n: 'ct', g: 1, val: 0, common: { type: 'number', min: 0, max: 5000, unit: '°K', desc: 'temperature in °Kelvin 0..5000', role: 'level.color.temperature', }, }, red: { n: 'r', g: 3, val: 0, common: { type: 'number', min: 0, max: 255, desc: '0..255 or #rrggbb[ww] (hex)', role: 'level.color.red' }, }, green: { n: 'g', g: 3, val: 0, common: { type: 'number', min: 0, max: 255, desc: '0..255 or #rrggbb[ww] (hex)', role: 'level.color.green' }, }, blue: { n: 'b', g: 3, val: 0, common: { type: 'number', min: 0, max: 255, desc: '0..255 or #rrggbb[ww] (hex)', role: 'level.color.blue' }, }, white: { n: 'w', g: 3, val: 0, common: { type: 'number', min: 0, max: 255, desc: '0..255 or #rrggbb[ww] (hex)', role: 'level.color.white' }, }, disco: { n: 'disco', g: 2, val: 1, common: { type: 'number', min: 1, max: 9, desc: '1..9' } }, progNo: { n: 'progNo', g: 1, val: 38, common: { type: 'number', min: 35, max: 56, desc: '37..56, 97=none' } }, progOn: { n: 'progOn', g: 1, val: false, common: { type: 'boolean', desc: 'program on/off' } }, progSpeed: { n: 'progSpeed', g: 3, val: 10, common: { type: 'number', min: 0, max: 255 }, desc: 'speed for program', }, refresh: { n: 'refresh', g: 1, val: false, common: { type: 'boolean', desc: 'read states from device' } }, transition: { n: 'trans', g: 1, val: 30, common: { type: 'number', unit: '\u2152 s', desc: 'in 10th seconds' } }, command: { n: 'command', g: 3, val: 'r:0, g:0, b:0, on:true, transition:30', desc: 'r:0, g:0, b:0, on:true, transition:2', }, rgb: { n: 'rgb', g: 3, val: '', common: { type: 'text', desc: '000000..ffffff', role: 'level.color.rgb' } }, onTime: { n: 'onTime', g: 3, val: '', common: {} }, }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function parseHexColors(val) { val = val.toString(); const ar = val.split('.'); if (ar && ar.length > 1) { val = ar[0]; } if (val[0] === '#') { val = val.substr(1); } const co = { r: parseInt(val.substr(0, 2), 16), g: parseInt(val.substr(2, 2), 16) || 0, b: parseInt(val.substr(4, 2), 16) || 0, //, }; if (val.length > 7) { co.w = parseInt(val.substr(6, 2), 16); } if (ar && ar.length > 1) { const m = Number(`.${ar[1]}`); for (const i in co) { co[i] *= m; } roundRGB(co); } return co; } function onStateChange(id, state) { if (!state || state.ack) { return; } const ar = id.split('.'); let deviceName = ar[2]; let channelName = ''; if (ar.length > 4) { channelName = ar.splice(3, 1)[0]; deviceName += `.${channelName}`; } const stateName = ar[3]; const device = wifi[deviceName]; if (device === undefined || !device.isOnline || !state || state.val === null || state.ack) { return; } if (device.cmds.decodeResponse) { devices.invalidate(id); } device.stopRunningProgram(); device.onStateChange(channelName, stateName, state.val); } function startAdapter() { adapter = utils.Adapter({ name: 'wifilight', message: obj => onMessage(obj), objectChange: (id, obj) => id && !obj && devices?.removeWithoutNameSpace(id), ready: () => checkIfUpdated(() => devices.init(adapter, () => main())), stateChange: (id, state) => onStateChange(id, state), unload: callback => onUnload(callback), }); return adapter; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class WifiLight { constructor(config) { if (!config) { throw new Error('no configuration'); } this.zone = undefined; this.config = config; this.isOnline = false; this.cmds = commands[config.type]; this.prgTimer = new Timer(); } run(cb) { if (!this.cmds) { adapter.log.error(`wrong device type. ${this.config.type} not yet supported!`); if (cb) { cb(-1); } return null; } if (this.cmds.vmax === undefined) { this.cmds.vmax = 255; } this.cmds.g = this.cmds.g || 1; this.createDevice((/* err */) => { if (this.cmds.onlyConnectOnWrite) { this.USE_SOCKET_ONCE = true; this.setOnline(null); } else { this.setOnline(false); } this.queue = []; this.dataBuffer = new Uint8Array(200); this.dataBuffer.pos = 0; this.states = { red: 0, green: 0, blue: 0 }; this.start(cb); }); return this; } log(msg) { adapter.log.debug(`[${this.config.ip}] ${msg}`); } createDevice(cb) { this.dev = new CDevice(0, devices); this.dev.setDevice(this.config.ip, { common: { name: this.config.name, role: 'device', }, native: { type: this.config.type, intervall: this.config.pollIntervall, }, }); if (this.zone !== undefined) { this.dev.setChannel(this.zone.toString(), ['All Zones', 'Zone 1', 'Zone 2', 'Zone 3', 'Zone 4'][this.zone]); } wifi[this.dev.getFullId()] = this; for (const j in usedStateNames) { if (j === 'white' && this.cmds.rgbw === undefined) { continue; } const st = Object.assign({}, usedStateNames[j]); if ((j === 'progNo' || j === 'disco') && this.cmds.programNames) { st.common.states = this.cmds.programNames; } if (st.g & this.cmds.g) { this.dev.createNew(st.n, st); } } devices.update(cb); } onStateChange(channel, stateName, val) { let transitionTime = this.getValue(channel, usedStateNames.transition.n, 0); this.clearQueue(); let co; switch (stateName) { case usedStateNames.transition.n: this.dev.setraw(usedStateNames.transition.n, val); break; case 'onTime': this.onTime(channel, val); break; case 'on': this.on_off(channel, !!(val >> 0)); break; case 'rgbw': case 'rgb': co = parseHexColors(val); this.color(channel, co); break; case 'r': case 'g': case 'b': case 'w': case 'sat': if (typeof val === 'string' && val[0] === '#') { co = parseHexColors(val); } else { co = this.getRGBStates(channel); co[stateName] = val >> 0; } this.color(channel, co); break; case usedStateNames.refresh.n: this.refresh(); this.dev.set(usedStateNames.refresh.n, false); this.dev.update(); break; case usedStateNames.bri.n: this.bri(channel, val >> 0, transitionTime); break; case usedStateNames.ct.n: this.ct(channel, val >> 0, transitionTime); break; case usedStateNames.progSpeed.n: { const progNo = this.get(channel, usedStateNames.progNo.n).val; this.addToQueue(channel, this.cmds.progNo, progNo, val); break; } case usedStateNames.progNo.n: { let speed; if (typeof val === 'string') { let ar = val.split(' '); if (!ar || ar.length < 2) { ar = val.split(','); } if (ar && ar.length >= 2) { speed = parseInt(ar[1]); val = parseInt(ar[0]); } } else { speed = this.getValue(channel, usedStateNames.progSpeed.n, 30); } this.addToQueue(channel, this.cmds.progNo, val >> 0, speed); break; } case usedStateNames.progOn.n: this.addToQueue(channel, val ? this.cmds.progOn : this.cmds.progOff); break; case usedStateNames.command.n: { let v = val.replace(/(^on$|red|green|blue|transition|bri|off)/g, match => { return { '#': '#', off: 'off:1', on: 'on:1', red: 'r', green: 'g', blue: 'b', white: 'w', transition: 'x', bri: 'l', //, off: 'on:0' }[match]; }); v = v .replace(/\s|"|;$|,$/g, '') .replace(/=/g, ':') .replace(/;/g, ',') .replace(/true/g, 1) .replace(/((on|off),{1})/g, '$2:1,') .replace(/#((\d|[a-f]|[A-F]|[.])*)/g, 'h:"$1"') .replace(/(r|g|b|w|x|l|sat|off|on|ct|h|p)/g, '"$1"') .replace(/^\{?(.*?)}?$/, '{$1}'); let colors; try { colors = JSON.parse(v); } catch (e) { adapter.log.error(`on Command: ${e.message}: state.val="${val}"`); return; } if (colors.p) { setTimeout(this.runJsonProgram.bind(this), 10, channel, colors.p); return; } if (colors.h) { Object.assign(colors, parseHexColors(colors.h)); delete colors.h; } if (!colors || typeof colors !== 'object') { return; } if (colors.off !== undefined) { this.color(channel, { r: 0, g: 0, b: 0, w: colors.w !== undefined ? 0 : undefined }); this.states.red = 0; this.states.green = 0; this.states.blue = 0; if (this.states.white !== undefined) { this.states.white = 0; } } const o = fullExtend(this.getRGBStates(channel), colors); adapter.log.debug(JSON.stringify(o)); if (o.x !== undefined) { transitionTime = o.x >> 0; } if (o['on'] !== undefined) { this.on_off(channel, !!(o.on >> 0)); } if ( colors.r !== undefined || colors.g !== undefined || colors.b !== undefined || colors.w !== undefined || colors.sat !== undefined ) { this.fade(channel, o, transitionTime); } if (o['ct'] !== undefined) { this.ct(channel, o.ct >> 0, transitionTime); } if (o['l'] !== undefined) { this.bri(channel, o.l >> 0, transitionTime); } break; } default: return; } } stopRunningProgram() { this.prgTimer.clear(); this.refreshPaused = 0; this.clearQueue(); } runJsonProgram(channel, cmds) { let i = -1; // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; const lastCo = { red: self.states.red, green: self.states.green, blue: self.states.blue }; this.prgTimer.clear(); self.clearQueue(); function doIt() { if (self.queue.length > 0) { this.doItTimeout && clearTimeout(this.doItTimeout); this.doItTimeout = setTimeout(() => { this.doItTimeout = null; doIt(); }, self.queue.length * 2); return; } if (++i >= cmds.length) { i = 0; } const cmd = cmds[i]; if (cmd.x === undefined) { cmd.x = 0; } const delay = Math.abs(cmd.x); if (cmd.r !== undefined) { Object.assign(self.states, lastCo); self.fade(channel, cmd, delay); lastCo.red = cmd.r; lastCo.green = cmd.g; lastCo.blue = cmd.b; } if (cmd.x < 0) { return; } self.prgTimer.set(doIt, 10 + delay * 10); } if (cmds.length > 0) { this.refreshPaused = true; doIt(); } else { this.stopRunningProgram(); } } reconnect(cb, timeout) { if (cb && typeof cb !== 'function') { timeout = cb; cb = undefined; } this.destroyClient(); this.reconnectTimeout = this.reconnectTimeout || setTimeout( () => { this.reconnectTimeout = null; this.start(cb); }, timeout === undefined ? 5000 : timeout, ); } _write(data, cb) { if (this.USE_SOCKET_ONCE) { this.writeOnce(data, cb); } else { this.client.write(data, cb); } } start(cb) { this.destroyClient(); if (debug) { this.ts = Date.now(); } this.client = new net.Socket(); this.client.on('data', data => this.onData(data)); this.client.on('close', hasError => { this.setOnline(false); const ts = debug ? `(${Math.round((Date.now() - this.ts) / 1000)} sec) ` : ''; this.log(`onClose ${ts}hasError=${hasError} client=${this.config.ip}:${this.config.port}`); }); this.client.on('error', error => { switch (error.code) { case 'ECONNRESET': case 'ETIMEDOUT': case 'EHOSTUNREACH': 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); }); this.client.connect(this.config.port, this.config.ip, () => { this.log(`${this.config.ip} connected`); this.setOnline(true); this.runUpdateTimer(); adapter.log.debug('self.client.connect: connected'); if (typeof cb == 'function') { cb(); } }); } destroyClient() { if (this.updateTimer) { 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; } } writeOnce(data, cb) { if (this.client) { this.client.write(data, cb); return; } this.client = new net.Socket(); this.client.on('data', data => { this.onData(data); this.client.end(); this.client = null; }); this.client.on('error', (/* error */) => this.destroyClient()); this.client.connect(this.config.port, this.config.ip, () => this.client.write(data, cb)); } get(channel, state) { return this.dev.get(channel, state); } getValue(channel, state, def) { const o = this.dev.get(channel, state); if (o?.val !== undefined) { return o.val; } return def; } close() { this.clearQueue(); this.destroyClient(); } runUpdateTimer() { if (!this.cmds.decodeResponse) { return; } this.refresh(); if (this.config.pollIntervall > 0) { this.updateTimer = setTimeout(() => this.runUpdateTimer(), this.config.pollIntervall * 1000); } } setOnline(val) { this.isOnline = val; if (!(this.cmds.g & usedStateNames.online.g)) { return; } this.dev.set(usedStateNames.online.n, val); devices.update(); } directRefresh(channel) { if (!this.cmds.statusRequest || this.refreshPaused) { return; } this.log('sending refresh...'); this.write(channel, this.cmds.statusRequest); } refresh(channel, ctrl) { if (!this.cmds.statusRequest || this.refreshPaused) { return; } this.addToQueue(channel, this.cmds.statusRequest, { ctrl: ctrl === undefined ? true : ctrl }); } write(channel, cmd, cb) { let buf; if (this.cmds.useCheckSum) { buf = Buffer.alloc(cmd.length + 1); let sum = 0; for (let i = 0; i < cmd.length; i++) { buf[i] = cmd[i]; sum += buf[i]; } buf[buf.length - 1] = sum & 0xff; } else { buf = Buffer.from(cmd); } //const s = buf.inspect(); //this.log('writing: ' + buf.toString('hex').match(/.{2}/g).join(' ')); this.log(`write: ${arrayToHex(buf)}`); if (!this.isOnline /*&& !this.USE_SOCKET_ONCE*/) { this.reconnect(() => this._write(buf, cb), 0); return; } this._write(buf, cb); } clearQueue() { this.queue.length = 0; } addToQueue(...args) { let channel = ''; let idx = 0; let cmd = []; if (!(args[0] instanceof Array)) { channel = args[0]; idx = 1; } if (!(args[idx] instanceof Array)) { return; } let j = idx + 1; if (args.length > j) { for (let i = 0; i < args[idx].length; i++) { cmd[i] = args[idx][i] < 0 && args[idx][i] !== commands.VARS.separator && args[idx][i] !== commands.VARS.sepNoDelay ? args[j++] : args[idx][i]; } } else { cmd = args[idx]; } let opt; if (args.length >= j && typeof args[j] == 'object') { opt = args[j]; } let _cmd = []; const last = cmd.length - 1; cmd.forEach((c, i) => { let sep = 0; switch (c) { case commands.VARS.separator: sep = 1; break; case commands.VARS.sepNoDelay: sep = 2; break; default: _cmd.push(c); } if (sep || i === last) { this.queue.push({ cmd: _cmd, ctrl: !!opt?.ctrl, channel: channel, delay: sep & 2 ? 0 : opt && opt.delay !== undefined ? opt.delay : this.cmds.delay !== undefined ? this.cmds.delay : 10, ts: 0, inProcess: 0, }); _cmd = []; } }); if (this.queue.length && this.queue[0].inProcess === 1) { return; } this.exec(); } exec() { let akt; do { if (this.queue.length <= 0) { return; } akt = this.queue[0]; if (!(akt.inProcess || (!akt.ctrl && akt.ts && akt.ts < Date.now()))) { break; } if (this.queue.length <= 1 && !compareArrays(akt.cmd, this.cmds.statusRequest)) { this.directRefresh(akt.channel); } this.queue.shift(); } while (this.queue.length); this.write(akt.channel, akt.cmd, () => (this.writeTimeout = setTimeout(() => this.exec(), akt.delay))); akt.inProcess = 1; } on_off(channel, state) { this.addToQueue(channel, state ? this.cmds.on : this.cmds.off); } fade(channel, rgbw, transitionTime) { if (!transitionTime) { this.color(channel, rgbw); return; } const co = { r: this.states.red, g: this.states.green, b: this.states.blue, w: this.states.white, }; const dif = { r: rgbw.r - co.r, g: rgbw.g - co.g, b: rgbw.b - co.b, }; dif.w = rgbw.w !== undefined && co.w !== undefined ? rgbw.w - co.w : 0; let maxSteps = Math.max(Math.abs(dif.r), Math.abs(dif.g), Math.abs(dif.b), Math.abs(dif.w), 1); maxSteps = Math.min((transitionTime * 100) / this.cmds.delay, maxSteps); dif.r /= maxSteps; dif.g /= maxSteps; dif.b /= maxSteps; dif.w /= maxSteps; const steps = maxSteps; const delay = Math.round((transitionTime * 100) / maxSteps); for (let i = 0; i < steps; i++) { co.r += dif.r; co.g += dif.g; co.b += dif.b; if (co.w !== undefined) { co.w += dif.w; } this.color(channel, roundRGB(co, true), { delay }); } } color(channel, rgbw, opt) { rgbw.w === undefined ? this.addToQueue(channel, this.cmds.rgb, rgbw.r, rgbw.g, rgbw.b, opt) : this.addToQueue(channel, this.cmds.rgbw, rgbw.r, rgbw.g, rgbw.b, rgbw.w, opt); } ct(channel, temp, transitionTime) { let co = ct2rgb(temp); const hsv = rgb2hsv(co); // hsv.v = this.get(channel, 'bri').val; const v = this.get(channel, 'bri').val; if (v) { hsv.v = v; } co = hsv2rgb(hsv); this.fade(channel, co, transitionTime); } getRGBStates(/* channel */) { return { r: this.states.red, g: this.states.green, b: this.states.blue, w: this.states.white, }; } bri(channel, bri, transitionTime) { let co = this.getRGBStates(channel); const hsv = rgb2hsv(co); hsv.v = Math.max(Math.min(bri, 100), 0); co = hsv2rgb(hsv); this.fade(channel, co, transitionTime); } onTime(channel, val) { if (this.onTimerObject) { clearTimeout(this.onTimerObject); this.onTimerObject = null; } let timeout = val >> 0; let cmd = '#00000000;x10'; if (typeof val == 'string') { const ar = val.split(';'); timeout = parseInt(ar.shift()); cmd = ar.join(';'); } if (timeout && timeout > 0) { this.onTimerObject = setTimeout(this.onStateChange.bind(this), timeout * 100, channel, 'command', cmd); } } onData(data) { this.setOnline(true); if (adapter.common.loglevel === 'debug') { adapter.log.debug(`raw data length: ${data.length}`); adapter.log.debug(`raw data: ${arrayToHex(data)}`); } const newPos = this.dataBuffer.pos + data.length; if (newPos > this.dataBuffer.length) { const b = new Uint8Array(newPos + 200); //const b = new Buffer(newPos + 200); for (let i = 0; i < this.dataBuffer.pos; i++) { b[i] = this.dataBuffer[i]; } b.pos = this.dataBuffer.pos; this.dataBuffer = b; } this.dataBuffer.set(data, this.dataBuffer.pos); this.dataBuffer.pos += data.length; while (this.dataBuffer.pos >= this.cmds.responseLen || this.dataBuffer.pos >= this.cmds.responseLen2) { const [lengthRead, states] = this.cmds.decodeResponse(this.dataBuffer); this.log(`onData: raw: ${arrayToHex(this.dataBuffer, lengthRead)}`); this.dataBuffer.copyWithin(0, lengthRead, this.dataBuffer.pos); this.dataBuffer.pos -= lengthRead; if (!states) { break; } this.states = states; this.log(`onData: ${JSON.stringify(this.states)}`); if (this.states) { this.dev.set(usedStateNames.on.n, this.states.on); this.dev.set(usedStateNames.red.n, this.states.red); this.dev.set(usedStateNames.green.n, this.states.green); this.dev.set(usedStateNames.blue.n, this.states.blue); this.dev.set(usedStateNames.progNo.n, this.states.progNo); this.dev.set(usedStateNames.progOn.n, this.states.progOn); this.dev.set(usedStateNames.progSpeed.n, this.states.progSpeed); this.dev.set(usedStateNames.white.n, this.states.white); let rgb = `#${this.states.red.toString(16).padStart(2, '0')}${this.states.green.toString(16).padStart(2, '0')}${this.states.blue.toString(16).padStart(2, '0')}`; if (this.states.white !== undefined) { rgb += this.states.white.toString(16).padStart(2, '0'); } this.dev.set(usedStateNames.rgb.n, rgb); devices.update(); } } return this.states; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class MiLight extends WifiLight { constructor(config, zone) { super(config); if (!this.cmds) { return; } this.zone = zone; this.cmds = clone(this.cmds); this.cmds.setZone(this.zone); this.states = { on: 0, red: 0, green: 0, blue: 0, white: 0 }; this.writeTimer = null; this.isOnline = null; } // writeUdp _write(data, cb) { if (!this.client) { const dgram = require('node:dgram'); this.client = dgram.createSocket('udp4'); this.client.on('listening', error => { if (error) { return cb && cb(error); } if (this.config.ip === '255.255.255.255') { this.client.setBroadcast(true); } }); this.client.on('message', (/* data, rInfo */) => {}); this.client.on('error', (/* error */) => {}); this.client.on('close', (/* error */) => { this.client = null; adapter.log.debug('UDP socked closed'); }); } this.client.send(data, 0, data.length, this.config.port, this.config.ip, (/* error, bytes */) => { this.writeTimer = setTimeout(() => { this.writeTimer = null; this?.client?.close(); }, 2000); cb && cb(); }); } bri(channel, bri /* , transitionTime */) { this.addToQueue(channel, this.cmds._bri(bri)); } color(channel, rgbw /* , opt */) { if (rgbw.w !== undefined) { this.addToQueue(channel, this.cmds._white(((rgbw.w * 100) / 255) >> 0)); return; } const hsv = rgb2hsv(rgbw); if (hsv.h === 0 && hsv.v === 0) { this.on_off(channel, false); return; } const color = (256 + 176 - Math.floor((Number(hsv.h) / 360.0) * 255.0)) % 256; this.addToQueue(channel, this.cmds.on); this.addToQueue(channel, this.cmds._color(color)); this.addToQueue(channel, this.cmds._bri(hsv.v)); } // pair(channel) { // for (let i = 0; i < 3; i++) { // this.addToQueue(channel, this.pair, { delay: 1000 }); // } // } // // unPair(channel) { // for (let i = 0; i < 15; i++) { // this.addToQueue(channel, this.unPair, { delay: 200 }); // } // } onStateChange(channel, stateName, val) { switch (stateName) { case 'disco': { val = val >> 0; if (val === 0) { this.addToQueue(channel, this.cmds.off); return; } // const bri = this.getValue(channel, 'bri'); let cmd = this.cmds._white(10).cc(this.cmds.on); while (val--) { cmd = cmd.cc(this.cmds.discoMode); } this.addToQueue(channel, cmd /*, {delay: 50}*/); break; } default: super.onStateChange(channel, stateName, val); break; } } } async function checkDeletedDevices() { const res = await adapter.getDevicesAsync(); if (!res?.length) { return; } const reIp = /[^0-9]/g; const toDelete = []; res.forEach(obj => { const ar = obj._id.split('.'); const ip = ar[2].replace(reIp, '.'); const found = adapter.config.devices.find(v => v.ip === ip); if (!found) { toDelete.push(obj._id); } }); for (let i = 0; i < toDelete.length; i++) { await delObjectWithStates(toDelete[i]); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// async function normalizeConfig(config) { let changed = false; const types = []; config.devices.forEach(d => { const old = Object.assign({}, d); const dev = fromDeviceName(d.name); if (d.type === undefined) { d.type = dev ? dev.type : ''; } const c = commands[d.type]; if (!c) { let err = `config.device.type "${d.type}" (${d.name}) is not a known device type. Skipping this device!`; if (!types.length) { Object.keys(commands).forEach(n => { if (typeof commands[n] === 'object' && commands[n].on) { types.push(n); } }); } err += `\nKnown types are: ${types.join(', ')}`; adapter.log.error(err); return; } if (d.pollIntervall === undefined) { d.pollIntervall = 30; } d.pollIntervall = parseInt(d.pollIntervall, 10) || 0; if (d.pollIntervall && d.pollIntervall < 5) { d.pollIntervall = 5; } d.port = parseInt(d.port) || (c && c.port ? c.port : dev && dev.port ? dev.port : 5577); Object.keys(d).forEach(key => (changed = changed || d[key] !== old[key])); }); if (changed) { await changeAdapterConfig(adapter, conf => (conf.devices = config.devices)); } } async function main() { if (!adapter.config.devices) { return; } await checkDeletedDevices(); await normalizeConfig(adapter.config); for (let i = 0; i < adapter.config.devices.length; i++) { if (adapter.config.devices[i].type === 'MiLight') { for (let zone = 0; zone <= 4; zone++) { const device = new MiLight(adapter.config.devices[i], zone); device.run(); } } else { const wifiLight = new WifiLight(adapter.config.devices[i]); wifiLight.run(); } } devices.update(); adapter.subscribeStates('*'); } // If started as allInOne mode => return function to create instance if (module.parent) { module.exports = startAdapter; } else { // or start the instance directly startAdapter(); }