UNPKG

iobroker.broadlink2

Version:
1,669 lines (1,581 loc) 64 kB
/* eslint-disable no-await-in-loop */ "use strict"; /** * UDP Connector for Broadlink devices * Modified version for ioBroker compatibility. Based on https://github.com/momodalo/broadlinkjs/blob/master/index.js * * @licence MIT */ // jshint node:true, esversion:6, strict:true, undef:true, unused:true, bitwise: false //var util = require('util'); const EventEmitter = require("events"), dgram = require("dgram"), dns = require("dns"), os = require("os"), crypto = require("crypto"), A = require("./fjadapter"); class Udp extends EventEmitter { constructor(port, address) { super(); this.retry = 3; this.cs = null; this.address = address; this.port = port; this._bound = false; this._ready = this.createSocket().catch(A.nothing); } async createSocket() { const that = this; this._bound = false; const cs = dgram.createSocket({ type: "udp4", reuseAddr: true, }); this.cs = cs; // this.address = cs.address(); cs.on("error", (err) => { A.I(`UDP Socket error ${err}`); that.renew(); }); // cs.on('listening', ); cs.on("message", (msg, rinfo) => { // A.I(`Message received on ${that.address}:${that.port} = '${msg.length}', ${A.O(rinfo)}`); that.emit("message", msg, rinfo); }); that._bound = await new Promise((res, rej) => { try { const pa = { exclusive: false, }; if (that.port) pa.port = that.port; if (that.port !== undefined && that.address) pa.address = that.address; // A.I(`Try to bind ${A.O(cs)} to ${that.address}:${that.port}`); cs.bind(pa, () => { const addr = cs.address(); that.retry = 3; that.port = addr.port; that.address = addr.address; that._bound = true; A.D(`UDP listening on ${that.address}:${that.port}`); res(true); }); } catch (e) { A.W(`could not bind socket ${e} for ${that.address}:${that.port}`); rej(false); } }); return that.bound; } toString() { return `Udp(${this.address}:${this.port}=${this._bound})`; } close() { if (this.cs && this.bound) { this.cs.removeAllListeners(); this.cs.close(); this.cs = null; this._bound = false; } } send(...args) { const that = this; // A.I(`Send ${args} via ${this.address}:${this.port}`); if (this.cs && this._bound) return new Promise((res, rej) => that.cs.send(...args, (err, obj) => { if (err) { A.I(`Send error ${err} on ${that}`); return rej(err); } res(obj); }) ); return Promise.resolve(null); } async renew(n) { if (n) this.retry = n; if (this._bound) this.close(); this.cs = null; while (this.retry > 0) { this.retry--; if (await this.createSocket()) break; } } get socket() { return this.cs; } get bound() { return this.cs && this._bound; } } const authMinutes = 5; function msMinutes(x) { const m = x < 0 ? authMinutes + x : x | authMinutes; return m * 60 * 1000; } class Device extends EventEmitter { constructor(host, mac, devtype, bl) { super(); // var self = this; this._val = {}; this.reAuth = Date.now() - msMinutes(-3); // this.s = new A.Sequence(); this.bl = bl; this.host = host; // this.type = "Unknown"; // this.typ = "UK"; delete this.host.family; delete this.host.size; host.mac = mac; this.devtype = devtype; this.host.devhex = Broadlink.toHex(devtype, 4); this.host.name = this.host.devhex + "_" + mac; this._cmdByte = 0; this.count = Math.random() & 0xffff; this.key = new Buffer([ 0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02, ]); this.id = new Buffer([0, 0, 0, 0]); this.iv = new Buffer([ 0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, ]); this.udp = new Udp(); /* this.cs = dgram.createSocket({ type: 'udp4', reuseAddr: true }); this.cs.on('close', function () { A.I('closed ' + A.O(self.host)); self.emit('close'); this.cs = null; }); try { this.cs.bind({ exclusive: false }); this.bound = true; } catch (e) { A.W(`could not bind socket for ${A.O(this.host)}`); } */ } get type() { const name = this.constructor.name; return name == "Device" ? "Unknown" : this.udp ? name : "closed"; } get typ() { return this.type.slice(0, 2); } static get errors() { return { 0xffff: "Authentication failed", 0xfffe: "You have been logged out", 0xfffd: "The device is offline", 0xfffc: "Command not supported", 0xfffb: "The device storage is full", 0xfffa: "Structure is abnormal", 0xfff9: "Control key is expired", 0xfff8: "Send error", 0xfff7: "Write error", 0xfff6: "Read error", 0xfff5: "SSID could not be found in AP configuration", }; } get bound() { return this.udp && this.udp.bound; } get doReAuth() { if (this.learning || !this.udp || this.inReAuth) return false; return ( (this.type.startsWith("RM4") || this.type.startsWith("LB")) && Date.now() - this.reAuth > msMinutes() ); } checkError(res, index) { let err = ""; // if (!res) err = "No result delivered! No err check possible!"; if (!res || !res.response) { return null; } // return "No result delivered! No err check possible!"; const pl = res.response; if (pl && pl.length > index + 1) { const e = pl[index] + (pl[index + 1] << 8); // const e = 0xfffb; // A.I(`${this.host.name}: e=${e.toString(16)}, f=${f.toString(16)}, res=${A.O(res)}`); if (e) { err = Device.errors[e] ? Device.errors[e] : `Unknown error ${e.toString(16)} in response!`; A.Df( "Dev %s returned err `%s` Check response from %s: %s, res=%s, host=%s", this.toString(), err, "0x" + index.toString(16), res.response.slice(index).toString("hex"), A.O(res), A.O(this.host) ); if (this.host.devtype == 0x5f36 && e == 0xfffb) { // A.I(`This.device had 0xfffb: The device storage error!`); err = ""; } else A.D(`host=${A.O(this.host)}, e=${e.toString(16)}`); if (e == 0xfff9) { // A.I(`This.device had 0xfff9: please re-auth!`); this.reAuth = Date.now() - msMinutes(); } } } if (!!err && res) res.err = err; if (err) A.D(`Error '${err}' in device.checkError for ${this}`); return err; } toString() { return `${this.type}, ${this.name}, ${this.host.mac}, ${this.host.address}${ this.host.oname ? ", " + this.host.oname : "" }${this.host.fware ? ", fw=" + this.host.fware.toString(16) : ""}${ this.host.cloud ? "cloud=true" : "" }`; } close() { if (this.tout) { // clearTimeout(self.tout); this.tout = null; } if (this.udp) { this.udp.close(); // this.cs.removeAllListeners(); this.udp = null; } if (this.bl && this.bl._devices) this.bl._devices[this.host.mac] = null; // this.type = 'closed'; this.emit("close"); } get dummy() { return !this.bound || this.type === "Unknown" || this.type === "closed"; } get val() { return this._val; } async checkOff(fun, ...args) { if (this.dummy) return { here: false, err: "closed", }; if (!fun || typeof fun != "function") return { err: "no function", }; try { const res = await fun.bind(this)(...args); if (res) return res; } catch (e) { try { await A.wait(10); return fun.bind(this)(...args); } catch (er) { A.D(`Error '${er}' when reading device ${this}`); return { err: er, }; } } // return Promise.resolve(fun ? fun.bind(this)(...args) : undefined); } async getAll() { if (A.T(this._val) === "object") this._val.here = false; let v = await this.checkOff(this.getVal); if (v === null || v === undefined) v = { here: false, }; else if (A.T(v) !== "object") v = { val: v, here: true, }; return v; } async getVal() { await A.wait(0); return this._val; } async setVal(obj) { await A.wait(0); return (this._val = obj); } async _send(packet) { const cmd = packet[this._cmdByte]; // const cmd = packet[0]; const timeout = this.timeout || 1000; const self = this; self.sent = new A.HrTime(); let count = 4; while (!this.bound && count--) { await A.wait(2); if (this._ready) await this._ready.then( () => true, () => false ); if (!this.bound && !count) { // debugger; const msg = `socket not created/bound/closed ${this}, ${this.udp}!`; A.W(msg); throw new Error(msg); } } count = 4; while (self.tout) { if (!count--) { const msg = `${this} still waiting for previous command ${this.tout}!`; A.W(msg); return Promise.reject(null); } await A.wait(20 + (4 - count) * 100); } this.udp.removeAllListeners("message"); // await this.s.catch(e => A.Dr(e, 'Something went wrong in previous send: %O', e)); return new Promise((res, rej) => { function reject(what) { if (self.tout) { clearTimeout(self.tout); } self.tout = null; self.udp.removeAllListeners("message"); rej(what); } function resume(what) { // self.udp.removeAllListeners("message"); res(what); } self.udp.once("message", async (response) => { if (self.tout) { clearTimeout(self.tout); } self.tout = null; // await A.nextTick(); // A.D(`Send took ${self.sent.text}s for ${self} `); self.lastResponse = Date.now(); const enc_payload = Buffer.alloc(response.length - 0x38, 0); response.copy(enc_payload, 0, 0x38); const decipher = crypto.createDecipheriv( "aes-128-cbc", self.key, self.iv ); decipher.setAutoPadding(false); let payload = decipher.update(enc_payload); var p2 = decipher.final(); if (p2) { payload = Buffer.concat([payload, p2]); } const command = response[0x26]; let err = response[0x22] | (response[0x23] << 8); // if (err == 0xfff9) this.reAuth = Date.now() - msMinutes(); // if (Device.errors[err]) { // err = Device.errors[err]; // } const obj = { cmd, command: command, // err, response, cmdHex: Broadlink.toHex(command), payload: payload, }; // A.If('message received, err=%s: %O',Broadlink.toHex(err), obj); if (command === 7) { A.If( "message command=7 received, err=%s: %O", Broadlink.toHex(err), obj ); return resume(obj); } // A.If('received message from %s:%O',self.name,obj); // if (!err) return resume(obj); // obj.err = err; // A.Wf(`Got error %O from device %s`, obj,self.host.name) // return reject(obj); }); self.tout = setTimeout( () => reject({ here: false, err: `timed out on send`, name: self.host.name, }), timeout ); return self.udp .send(packet, 0, packet.length, self.host.port, self.host.address) .then( (r) => r, (r) => r ) .then((r) => r ? r : reject({ here: false, err: `send udp packet error`, name: self.host.name, }) ); }); } async auth() { const self = this; this.key = new Buffer([ 0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02, ]); this.id = new Buffer([0, 0, 0, 0]); let payload = Buffer.alloc(0x50, 0); payload[0x04] = 0x31; payload[0x05] = 0x31; payload[0x06] = 0x31; payload[0x07] = 0x31; payload[0x08] = 0x31; payload[0x09] = 0x31; payload[0x0a] = 0x31; payload[0x0b] = 0x31; payload[0x0c] = 0x31; payload[0x0d] = 0x31; payload[0x0e] = 0x31; payload[0x0f] = 0x31; payload[0x10] = 0x31; payload[0x11] = 0x31; payload[0x12] = 0x31; payload[0x1e] = 0x01; payload[0x2d] = 0x01; payload[0x30] = "T".charCodeAt(0); payload[0x31] = "e".charCodeAt(0); payload[0x32] = "s".charCodeAt(0); payload[0x33] = "t".charCodeAt(0); payload[0x34] = " ".charCodeAt(0); payload[0x35] = " ".charCodeAt(0); payload[0x36] = "1".charCodeAt(0); const what = await this.sendPacket(0x65, payload); self.reAuth = Date.now() - msMinutes(-1); if (this.checkError(what, 0x22)) return Promise.reject(what); const command = what.command; payload = what.payload; if (command === 0xe9) { // A.If('auth payload: %O', payload); self.key = Buffer.alloc(0x10, 0); payload.copy(self.key, 0, 0x04, 0x14); self.id = Buffer.alloc(0x04, 0); payload.copy(self.id, 0, 0x00, 0x04); // A.I(`I emit deviceReady for ${A.O(self.host)}`); self.reAuth = Date.now(); A.N(self.emit.bind(self), "deviceReady", self); // self.emit("deviceReady", self); } else { if (!what.err) what.err = 0xe9; return Promise.reject(what); } return self; // .catch(e => A.Df('catch auth error %s! on %s with mac %s', e, self.host.name, self.host.address)); } async sendPacket(command, payload, timeout) { const that = this; const cmd = payload && Number(payload[0]); if (this.doReAuth) { that.inReAuth = true; that.reAuth = Date.now() - msMinutes(-2); A.wait(2000).then(async () => { // A.D(`Need to re-auth ${that}!`); const res = await A.retry(3, that.auth.bind(that), 100).catch((err) => A.W(`Failed to authenticate device ${that} with err ${err}`) ); A.D( `Reauth result of ${that} is: ${res} ${ (Date.now() - that.reAuth) / 1000.0 } seconds` ); }); that.inReAuth = false; } this.timeout = timeout || 700; if (this.timeout < 0) { this.timeout = -this.timeout; } this.count = (this.count + 1) & 0xffff; var packet = Buffer.alloc(0x38, 0); packet[0x00] = 0x5a; packet[0x01] = 0xa5; packet[0x02] = 0xaa; packet[0x03] = 0x55; packet[0x04] = 0x5a; packet[0x05] = 0xa5; packet[0x06] = 0xaa; packet[0x07] = 0x55; packet[0x24] = this.devtype & 0xff; packet[0x25] = this.devtype >> 8; packet[0x26] = command; packet[0x28] = this.count & 0xff; packet[0x29] = this.count >> 8; packet[0x2a] = this.host.maco[0]; packet[0x2b] = this.host.maco[1]; packet[0x2c] = this.host.maco[2]; packet[0x2d] = this.host.maco[3]; packet[0x2e] = this.host.maco[4]; packet[0x2f] = this.host.maco[5]; packet[0x30] = this.id[0]; packet[0x31] = this.id[1]; packet[0x32] = this.id[2]; packet[0x33] = this.id[3]; if (payload && payload.length > 0) { let npl = Buffer.alloc(parseInt((payload.length + 16) / 16) * 16, 0); payload.copy(npl, 0, 0); payload = npl; } var checksum = 0xbeaf, i; for (i = 0; i < payload.length; i++) { checksum += payload[i]; checksum = checksum & 0xffff; } var cipher = crypto.createCipheriv("aes-128-cbc", this.key, this.iv); payload = cipher.update(payload); // var p2 = cipher.final(); packet[0x34] = checksum & 0xff; packet[0x35] = checksum >> 8; packet = Buffer.concat([packet, payload]); checksum = 0xbeaf; for (i = 0; i < packet.length; i++) { checksum += packet[i]; checksum = checksum & 0xffff; } packet[0x20] = checksum & 0xff; packet[0x21] = checksum >> 8; let pb = new Buffer.alloc(packet.length - 0x26); packet.copy(pb, 0, 0x26); // A.If('sendPacket from id %s mac %s command %s, payload: %s, packet: %s, key:%s, iv:%s', this.id.toString('hex'), this.host.maco.toString('hex'), command.toString(16), payload.toString('hex'), pb.toString('hex'), this.key.toString('hex'), this.iv.toString('hex')); let err = null; if (timeout < 0) return this._send(packet, command).catch((e) => { err = "SendPacketSingleErr " + A.O(e); return null; }); let n = 3; while (n--) { // eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-loop-func let res = null; try { res = await this._send(packet, command); } catch (e) { A.Df("send Error on device %s: %O", this.toString(), e); // if (e && e.err) { // const err = e.err; // if ( // Device.errors[err] || // Object.entries(Device.errors).filter((i) => i[1] == err).length // ) { // A.I(`Unrecoverable Send packet error ${A.O(e)} on ${that}`); // return null; // } // } res = e; } if (res && !res.err) return res; if (res) err = res.err; // if (n == 2) // await this.udp.renew(3); await A.wait(20 + 150 * (3 - n)); } A.D( `sendPacket error: command ${"0x" + command.toString(16)}/${ "0x" + cmd.toString(16) } error after 3 trials!: ${err} for ${that}` ); // if (this.errorcount>10) await this.auth().catch(x => A.W(`Re-Auth failed with ${x} for ${this}`)); return null; // return A.retry(3, this._send.bind(this), packet); } /* def set_name(self, name): packet = bytearray(4) packet += name.encode('utf-8') packet += bytearray(0x50 - len(packet)) packet[0x43] = self.cloud response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) self.name = name def set_lock(self, state): packet = bytearray(4) packet += self.name.encode('utf-8') packet += bytearray(0x50 - len(packet)) packet[0x43] = state response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) self.cloud = bool(state) */ async get_firmware() { // const self = this; var packet = Buffer.alloc(16, 0); packet[0] = 0x68; const res = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(res, 0x22); // A.I(`getVal on '${this.constructor.name}' returned ${A.O(res)}`); if (res && res.payload && !res.err) { let payload = res.payload; const fw = payload[0x4] + (payload[0x5] << 8); this.host.fware = fw; // A.Df("Device has firmware %s: %O", fw.toString(16), this.host); return fw; } return NaN; } } class MP1 extends Device { // constructor(host, mac, devtype, bl) { // super(host, mac, devtype, bl); // // this.type = "MP"; // } async getVal() { //"""Returns the power state of the smart plug.""" const ret = this._val; if (A.T(ret) === "object") ret.here = false; var packet = Buffer.alloc(16, 0); // A.I(`getVal on '${this.constructor.name}' called! on ${A.O(this.host)}`); packet[0x00] = 0x0a; packet[0x02] = 0xa5; packet[0x03] = 0xa5; packet[0x04] = 0x5a; packet[0x05] = 0x5a; packet[0x06] = 0xae; packet[0x07] = 0xc0; packet[0x08] = 0x01; const res = await this.checkOff(this.sendPacket, 0x6a, packet); if (this.checkError(res, 0x22)); // A.I(`getVal on '${this.constructor.name}' returned ${A.O(res)}`); if (res && res.payload && !res.err) { let state = res.payload[0x0e]; ret.here = true; ret.sw1 = Boolean(state & 1); ret.sw2 = Boolean(state & 2); ret.sw3 = Boolean(state & 4); ret.sw4 = Boolean(state & 8); // ret.nightlight = !!(payload[0x4] & 2); } return ret; } async setVal(state, sw) { //"""Sets the power state of the smart plug.""" const vret = this._val; let st = state; if (!sw || isNaN(Number(sw)) || sw < 1 || sw > 4) return A.resolve( A.W(`call of setVal on ${this.host.name} with wrong argument 2: ${sw}`) ); // let nl = false; A.I(`setVal on '${this.host.name}' to ${A.O(state)}`); // if (A.T(state) === 'object' && state.nightlight !== undefined) // nl = !!state.nightlight; if (A.T(state) === "object" && state.state !== undefined) st = !!state.state; else if (typeof state === "boolean" || typeof state === "number") st = !!state; else return A.reject( `setVal on '${this.host.name}' to ${A.O( state )}: error wrong argument type!` ); let sid_mask = 1 << (sw - 1); let packet = Buffer.alloc(16, 0); packet[0x00] = 0x0d; packet[0x02] = 0xa5; packet[0x03] = 0xa5; packet[0x04] = 0x5a; packet[0x05] = 0x5a; packet[0x06] = 0xb2 + (st ? sid_mask << 1 : sid_mask); packet[0x07] = 0xc0; packet[0x08] = 0x02; packet[0x0a] = 0x03; packet[0x0d] = sid_mask; packet[0x0e] = st ? sid_mask : 0; const ret = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(ret, 0x22); if (!ret || ret.err || !ret.payload) return vret; return (vret["sw" + sw] = st); } } class SP1 extends Device { constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "SP1"; this._val = undefined; } async setVal(state) { let st = state; if (typeof state === "number" || typeof state === "boolean") state = { val: !!state, }; if (typeof state === "object") st = !!state.state; var packet = Buffer.alloc(4, 4); packet[0] = st ? 1 : 0; const ret = await this.checkOff(this.sendPacket, 0x66, packet); this.checkError(ret, 0x22); // console.log("setVal SP1", ret); if (!ret || ret.err || !ret.payload) return this._val; return (this._val = st); } } class SP2 extends Device { // eslint-disable-next-line no-useless-constructor constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "SP2"; } async getVal() { //"""Returns the power state of the smart plug.""" const ret = this._val; if (A.T(ret) === "object") ret.here = false; var packet = Buffer.alloc(16, 0); // A.I(`getVal on '${this.constructor.name}' called! on ${A.O(this.host)}`); packet[0] = 1; const res = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(res, 0x22); // A.I(`getVal on '${this.constructor.name}' returned ${A.O(res)}`); if (res && res.payload && !res.err) { let payload = res.payload; ret.here = true; ret.state = !!(payload[0x4] & 1); // ret.nightlight = !!(payload[0x4] & 2); } return ret; // eslint-disable-next-line no-unused-vars } async setVal(state) { //"""Sets the power state of the smart plug.""" const self = this; let st = state; // let nl = false; A.I(`setVal on '${this.host.name}' to ${A.O(state)}`); // if (A.T(state) === 'object' && state.nightlight !== undefined) // nl = !!state.nightlight; if (A.T(state) === "object" && state.state !== undefined) st = !!state.state; else if (typeof state === "boolean" || typeof state === "number") st = !!state; else return A.reject( `setVal on '${this.host.name}' to ${A.O( state )}: error wrong argument type!` ); let packet = Buffer.alloc(16, 0); packet[0] = 2; // packet[4] = (st ? 1 : 0) + (nl ? 2 : 0); packet[4] = st ? 1 : 0; const ret = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(ret, 0x22); // console.log("setVal SP2", ret); if (!ret || ret.err || !ret.payload) return self._val; return A.resolve((self._val.state = st)); } } class SP3P extends Device { constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = 'SP3P'; this._val = {}; } async setVal(state) { //"""Sets the power state of the smart plug.""" const self = this; let st = state; let nl = false; A.I(`setVal on '${this.host.name}' to ${A.O(state)}`); if (A.T(state) === "object" && state.nightlight !== undefined) nl = !!state.nightlight; if (A.T(state) === "object" && state.state !== undefined) st = !!state.state; else if (typeof state === "boolean" || typeof state === "number") st = !!state; else return A.reject( `setVal on '${this.host.name}' to ${A.O( state )}: error wrong argument type!` ); let packet = Buffer.alloc(16, 0); packet[0] = 2; packet[4] = (st ? 1 : 0) + (nl ? 2 : 0); const x = await this.getAll().catch(() => null); if (x) { self._val.energy = x.energy; self._val.nightlight = x.nightlight; self._val.state = x.state; } const ret = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(ret, 0x22); // console.log("setVal SP3P", ret); if (!ret || ret.err || !ret.payload) return self._val; self._val.state = st; self._val.here = true; return self._val; } async getVal() { //"""Returns the power state of the smart plug.""" const ret = this._val; if (A.T(ret) === "object") ret.here = false; var packet = Buffer.alloc(16, 0); // A.I(`getVal on '${this.constructor.name}' called! on ${A.O(this.host)}`); packet[0] = 1; const res = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(res, 0x22); // A.I(`getVal on '${this.constructor.name}' returned ${A.O(res)}`); if (res && res.payload && !res.err) { let payload = res.payload; ret.here = true; ret.state = !!(payload[0x4] & 1); ret.nightlight = !!(payload[0x4] & 2); } return ret; // eslint-disable-next-line no-unused-vars } async getAll() { const self = this; const ret = this._val; // A.I(`getAll on SP3P called! val = ${A.O(ret)}`); const val = await this.getVal(); Object.assign(ret, val); const energy = await self.getEnergy(); if (energy !== undefined && energy !== null) ret.energy = energy; self._val = ret; return ret; } async getEnergy() { //"""Returns the power state of the smart plug.""" // A.I(`calling get_energy on ${A.O(this.host)}`); const packet = Buffer.alloc(16, 0); packet[0] = 8; packet[2] = 254; packet[3] = 1; packet[4] = 5; packet[5] = 1; packet[9] = 45; const ret = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(ret, 0x22); // A.I(`payload get energy: ${A.O(ret)}`); if (ret && ret.payload && ret.payload[0] === 8) return ( parseFloat( ret.payload[7].toString(16) + ret.payload[6].toString(16) + ret.payload[5].toString(16) ) / 100.0 ); return this._val && this._val.energy; } } class A1 extends Device { constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "A1"; this._val = {}; } async getVal() { // const self = this; const ret = this._val; ret.here = false; var packet = Buffer.alloc(16, 0); // A.I(`getVal on '${this.constructor.name}' called! on ${A.O(this.host)}`); packet[0] = 1; const res = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(res, 0x22); // A.I(`getVal on '${this.constructor.name}' returned ${A.O(res)}`); if (res && res.payload && !res.err) { let payload = res.payload; ret.temperature = (payload[0x4] * 10 + payload[0x5]) / 10.0; ret.humidity = (payload[0x6] * 10 + payload[0x7]) / 10.0; ret.light = payload[0x8]; // "0:finster;1:dunkel;2:normal;3:hell" ret.air_quality = payload[0x0a]; // "0:sehr gut;1:gut;2:normal;3:schlecht" ret.noise = payload[0xc]; // "0:ruhig;1:normal;2:laut;3:sehr laut" ret.here = true; } return ret; } } class S1 extends Device { constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "S1"; this._val = {}; this._sensorTypes = { 0x31: "DoorSensor", // 49 as hex 0x91: "KeyFob", // 145 as hex, as serial on fob corpse 0x21: "MotionSensor", // 33 as hex }; } async getVal() { function getSensor(pl, num) { const val = {}; const buf = pl.slice(6 + num * 83, 5 + (num + 1) * 83); // val.payload = buf; // val.status = Number(buf[0]); // val.order = Number(buf[1]); // val.type = Number(buf[3]); let name = buf.slice(4, 26); let serial = buf[26] + buf[27] * 256 + buf[28] * 65536 + buf[29] * 16777216; while (!name[name.length - 1]) name = name.slice(0, [name.length - 1]); name = name.toString("utf8") + " " + serial.toString(16); name = name.replace(/\s+/g, "_"); val[name] = Number(buf[0]); return val; } // const self = this; const ret = this._val; ret.here = false; var packet = Buffer.alloc(16, 0); // A.I(`getVal on '${this.constructor.name}' called! on ${A.O(this.host)}`); packet[0] = 6; const res = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(res, 0x22); // A.I(`getVal on '${this.constructor.name}' returned ${A.O(res)}`); if (res && res.payload && !res.err) { // A.If('S1 %s returned len &d %O', self.host.name, res.payload.length, res.payload); let number = Number(res.payload[0x4]); for (let i = 0; i < number; i++) { // Object.assign(ret,getSensor(res.payload,i)); Object.assign(ret, getSensor(res.payload, i)); } } return ret; } } class RM extends Device { constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "RM"; this._request_header = Buffer.from([]); this._code_sending_header = Buffer.from([]); } async _readSensor(type, offset, divider) { const ret = this._val; if (A.T(ret) === "object") ret.here = false; var packet = Buffer.concat([this._request_header, Buffer.from([type])]); // A.I(`getVal on '${this.constructor.name}' called! on ${A.O(this.host)}`); const res = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(res, 0x22); if (res && res.payload && !res.err) { const off = this._request_header.length + offset; const payload = res.payload; // A.I(`_readSensor '${this.constructor.name}':${type},${offset},${divider} returned ${A.O(res)}`); ret.here = true; // A.I(`Payload receifed ${payload.toString('hex')} on ${this}`); return payload[off] + payload[off + 1] / divider; } else return undefined; } async getVal() { const ret = this._val; const res = await this._readSensor(0x01, 4, 10.0); if (res !== undefined) ret.temperature = res; return ret; } async checkData() { var packet = Buffer.concat([this._request_header, Buffer.from([0x04])]); // A.I(`send checkData on '${this.constructor.name}'`); const res = await this.checkOff(this.sendPacket, 0x6a, packet, -1000); // this.checkError(res, 0x22); // A.I(`checkData on '${this.constructor.name}' returned ${A.O(res)}`); if (res && res.payload && !res.err) { let data = res.payload.slice(this._request_header.length + 4); A.If( "checkData command %s:%s, %s, %d", "04", this, data.toString("hex"), data.length ); if (data && data.length <= 14) { let s = 0; for (let i = 0; i < data.length; i++) s += data[i]; if (!s) return null; } return data; } return null; } async learn(msg) { const self = this; if (msg) msg( `Start learning with ${this.host.name}: Please press button on remote in next 30 seconds!` ); this.learning = true; // A.If('Should learn on %s', this.host.name); const res = {}; // let first = true; // while (first && await self.checkData()) { // await A.wait(50); // first = false; // } await this.checkData().catch(() => null); await self.enterLearning(); for (let i = 30; i > 0; i--) { await A.wait(1000); const r = await self.checkData(); if (r) { const data = r.toString("hex"); // A.I("Received from device: " + data); res.data = data; break; } if (msg) msg(`Please press button on remote in next ${i - 1} seconds!`); } if (msg) msg( res.data ? `Learning finished and packet received!` : `Timeout: No data received when learning!` ); self.learning = false; return res; } async sendVal(data) { const self = this; const packet = Buffer.concat([ this._code_sending_header, Buffer.from([0x02, 0x00, 0x00, 0x00]), data, ]); const ret = await self.checkOff(self.sendPacket, 0x6a, packet, -1000); //.then(x => A.I(`setVal/sendData for ${this.host.name} returned ${A.O(x)}`, x)); return this.checkError(ret, 0x22); } async enterLearning() { A.If("enterLearning command %s:%s", "03", this); var packet = Buffer.concat([this._request_header, Buffer.from([0x03])]); const ret = await this.checkOff(this.sendPacket, 0x6a, packet); //.then(x => A.I(`enterLearning for ${this.host.name} returned ${A.O(x)}`, x)); return this.checkError(ret, 0x22); } } class RMP extends RM { // eslint-disable-next-line no-useless-constructor constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "RMP"; } async enterRFSweep(start) { const command = start ? 0x19 : 0x1e; const packet = Buffer.concat([ this._request_header, Buffer.from([command]), ]); A.If("enterRFSwwep command %s:%s", command.toString(16), this); // var packet = Buffer.alloc(16, 0); // packet[0] = start ? 0x19 : 0x1e; const ret = await this.checkOff(this.sendPacket, 0x6a, packet); //.then(x => A.I(`enterRFSweep for ${this.host.name} returned ${A.O(x)}`, x)); return this.checkError(ret, 0x22); } async checkRFData(check2) { const command = check2 ? 0x1b : 0x1a; // check2=true = fund_rf_packet, false= check_frequency A.If("CheckRFData command %s:%s", command.toString(16), this); const packet = Buffer.concat([ this._request_header, Buffer.from([command]), ]); // var packet = Buffer.alloc(16, 0); // packet[0] = check2 ? 0x1b : 0x1a; const res = await this.checkOff(this.sendPacket, 0x6a, packet); this.checkError(res, 0x22); if (res && res.payload && !res.err) { if (res.payload[this._request_header.length + 4] === 1) return true; } return null; } async learnRf(msg) { // const self = this; if (msg) msg( `Start RF-sweep with ${this.host.name}: Please press button on RF remote until frequency found!` ); // A.Df('Start learning with %s on %s', rf, self.host.name); this.learning = true; const l = {}; let f = false; await this.checkData().catch(() => null); await this.enterRFSweep(true); for (let i = 30; i > 0; i--) { await A.wait(1000); f = await this.checkRFData(false); // const r = await self.checkData(); if (f) break; if (msg) msg(`Continue to press button on RF remote for maximal ${i} seconds!`); } if (!f) { if (msg) msg(`Could not find frequency, will stop learning!`); await this.enterRFSweep(false); this.learning = false; return l; } if (msg) msg(`found Frequency 1 of 2, you can release button now!`); await A.wait(1000); if (msg) msg(`To complete learning single press button you want to lear now!`); await this.checkRFData(true); for (let i = 10; i > 0; i--) { await A.wait(1000); const data = await this.checkData(); if (data) { l.data = data.toString("hex"); break; } if (msg) msg( `Please single press button you want to learn in next ${ i - 1 } seconds!` ); } if (msg) msg( l.data ? `Found learned button!` : `Could not learn button, will exit learning now!` ); this.learning = false; // console.log(l); return l; } } class RM4 extends RM { constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); this._cmdByte = 2; // this.type = "RM4"; this._request_header = Buffer.from([0x04, 0x00]); this._code_sending_header = Buffer.from([0xd0, 0x00]); } async getVal() { const ret = this._val; const res = await this._readSensor(0x24, 4, 10.0); if (res !== undefined) ret.temperature = res; return ret; } async getHumidity() { const ret = this._val; const res = await this._readSensor(0x24, 6, 100.0); if (res !== undefined) ret.humidity = res; // delete ret.val; return ret; } async getAll() { const ret = await this.getVal(); const h = await this.getHumidity(); ret.humidity = h.humidity; return ret; } } class RM4P extends RMP { constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "RM4P"; this._request_header = Buffer.from([0x04, 0x00]); this._code_sending_header = Buffer.from([0xd0, 0x00]); } async getVal() { const ret = this._val; const res = await this._readSensor(0x24, 4, 10.0); if (res !== undefined) ret.temperature = res; return ret; } async getHumidity() { const ret = this._val; const res = await this._readSensor(0x24, 6, 100.0); if (res !== undefined) ret.humidity = res; // delete ret.val; return ret; } async getAll() { const ret = await this.getVal(); const h = await this.getHumidity(); ret.humidity = h.humidity; return ret; } } class T1 extends Device { // eslint-disable-next-line no-useless-constructor constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "T1"; } // --- start of other test async sendT1packet(cmd, data) { function crc16(buffer) { var crc = 0xffff; var odd; for (var i = 0; i < buffer.length; i++) { crc = crc ^ buffer[i]; for (var j = 0; j < 8; j++) { odd = crc & 0x0001; crc = crc >> 1; if (odd) { crc = crc ^ 0xa001; } } } return crc; } // if (!data) // data = Buffer.alloc(4, 0); // else if (Array.isArray(data)) data = Buffer.from(data); // A.If('sendT1packet: from id:%O key:%O = %O', this.id,this.key, data); const payload = Buffer.alloc(data.length + 4, 0); data.copy(payload, 2, 0); payload[0] = data.length + 2; let cc = crc16(data); payload[data.length + 2] = cc & 0xff; payload[data.length + 3] = (cc >> 8) & 0xff; // A.If('sendT1packet: %O', payload); const res = await this.sendPacket(cmd, payload); // this.checkError(res, 0x22); // A.If('sendT1packet got back from cmd %O',res); if (res && res.payload && !res.err) { // let pl = res.payload; // let size = pl[0]; // A.If('Got payload with size %n: %O', size, pl); return res.payload; } return null; } setTime(date) { if (!date) date = new Date(); let dow = date.getDay(); dow = dow ? dow : 7; return this.checkOff(this.sendT1packet, 0x6a, [ 0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, date.getHours(), date.getMinutes(), date.getSeconds(), dow, ]); } setMode(auto, loop, sensor) { auto = auto !== undefined ? auto : this._val.autoMode; loop = loop !== undefined ? loop : this._val.loopMode; sensor = sensor !== undefined ? sensor : this._val.sensor; let mode = loop * 16 + auto; sensor = sensor ? sensor : 0; A.Df( "setMode for %s = auto:%d loop:%d mode:%O, sensor:%O", this.name, auto, loop, mode, sensor ); return this.checkOff(this.sendT1packet, 0x6a, [ 0x01, 0x06, 0x00, 0x02, mode, sensor, ]); } /* def set_temp(self, temp): self.send_request(bytearray([0x01,0x06,0x00,0x01,0x00,int(temp * 2)]) ) */ setTemp(temp) { A.Df("setTemp for %s = temp:%d", this.name, temp); return this.checkOff(this.sendT1packet, 0x6a, [ 0x01, 0x06, 0x00, 0x01, 0x00, parseInt(temp * 2.0), ]); } /* def set_power(self, power=1, remote_lock=0): self.send_request(bytearray([0x01,0x06,0x00,0x00,remote_lock,power]) ) */ setPower(power, remote) { power = power !== undefined ? power : this._val.power; remote = remote !== undefined ? remote : this._val.remoteLock; remote = remote ? 1 : 0; power = power ? 1 : 0; A.Df("setPower for %s = power:%d remote:%d", this.name, power, remote); return this.checkOff(this.sendT1packet, 0x6a, [ 0x01, 0x06, 0x00, 0x00, remote, power, ]); } /* # Advanced settings # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor, 2 for internal control temperature, external limit temperature. Factory default: 0. # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C # Deadzone for floor temprature (dIF) dif = 1..9. Factory default: 2C # Upper temperature limit for internal sensor (SVH) svh = 5..99. Factory default: 35C # Lower temperature limit for internal sensor (SVL) svl = 5..99. Factory default: 5C # Actual temperature calibration (AdJ) adj = -0.5. Prescision 0.1C # Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down, 1 for anti-freezing function open. Factory default: 0 # Power on memory (POn) poweron = 0 for power on memory off, 1 for power on memory on. Factory default: 0 def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron): input_payload = bytearray([0x01,0x10,0x00,0x02,0x00,0x05,0x0a, loop_mode, sensor, osv, dif, svh, svl, (int(adj*2)>>8 & 0xff), (int(adj*2) & 0xff), fre, poweron]) self.send_request(input_payload) */ setAdvanced(item, val) { let v = this._val; if (item && v && v[item] !== undefined) this._val[item] = val; A.Df("setAdvanced for %s = power:%d remote:%d", this.name, item, val); return this.checkOff(this.sendT1packet, 0x6a, [ 0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, v.loopMode, v.sensor, v.osv, v.dif, v.svh, v.svl, (parseInt(v.roomTempAdj * 2) >> 8) & 0xff, parseInt(v.roomTempAdj * 2) & 0xff, v.fre, v.poweron, ]); } /* # Set timer schedule # Format is the same as you get from get_full_status. # weekday is a list (ordered) of 6 dicts like: # {'start_hour':17, 'start_minute':30, 'temp': 22 } # Each one specifies the thermostat temp that will become effective at start_hour:start_minute # weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon) def set_schedule(self,weekday,weekend): # Begin with some magic values ... input_payload = bytearray([0x01,0x10,0x00,0x0a,0x00,0x0c,0x18]) # Now simply append times/temps # weekday times for i in range(0, 6): input_payload.append( weekday[i]['start_hour'] ) input_payload.append( weekday[i]['start_minute'] ) # weekend times for i in range(0, 2): input_payload.append( weekend[i]['start_hour'] ) input_payload.append( weekend[i]['start_minute'] ) # weekday temperatures for i in range(0, 6): input_payload.append( int(weekday[i]['temp'] * 2) ) # weekend temperatures for i in range(0, 2): input_payload.append( int(weekend[i]['temp'] * 2) ) self.send_request(input_payload) */ setSchedule(id, val) { A.Df("Should set schedule item %s for %s to %O", id, val); let v = this._val; let a = [0x01, 0x10, 0x00, 0x0a, 0x00, 0x0c, 0x18]; let i; for (i = 0; i < 6; i++) { a.push(v.weekday[i].startHour); a.push(v.weekday[i].startMinute); } for (i = 0; i < 2; i++) { a.push(v.weekend[i].startHour); a.push(v.weekend[i].startMinute); } for (i = 0; i < 6; i++) { a.push(parseInt(v.weekday[i].temp * 2)); } for (i = 0; i < 2; i++) { a.push(parseInt(v.weekend[i].temp * 2)); } return this.checkOff(this.sendT1packet, 0x6a, a); } async getAll() { const ret = this._val; ret.here = false; function getSched(x, start, end) { let r = []; let i = start; while (i < end) { r.push({ startHour: Number(x[2 * i + 23]), startMinute: Number(x[2 * i + 24]), temp: Number(x[i + 39]) / 2.0, }); ++i; } return r; } // return this.sendT1packet(0xA0).then(x => { const x = await this.checkOff(this.sendT1packet, 0x6a, [ 0x01, 0x03, 0x00, 0x00, 0x00, 0x16, ]); if (x) { // A.If('full status payload: %O',x); x.copy(x, 0, 2); ret.here = true; // ret.payload = x; ret.remoteLock = Boolean(x[3] & 1); ret.power = Boolean(x[4] & 1); ret.active = Boolean(x[4] & 16); ret.tempManual = Boolean(x[4] & 64); ret.roomTemp = Number(x[5]) / 2.0; ret.thermostatTemp = Number(x[6]) / 2.0; ret.autoMode = Number(x[7] & 15); ret.loopMode = Number((x[7] >> 4) & 15); ret.sensor = Number(x[8]); ret.osv = Number(x[9]); ret.dif = Number(x[10]); ret.svh = Number(x[11]); ret.svl = Number(x[12]); ret.roomTempAdj = (Number(x[13]) * 256 + Number(x[14])) / 2.0; if (ret.roomTempAdj > 32767) ret.roomTempAdj = 32767 - ret.roomTempAdj; ret.fre = Number(x[15]); ret.poweron = Number(x[16]); ret.unknown = Number(x[17]); ret.externalTemp = Number(x[18]) / 2.0; ret.time = Number(x[19]) + ":" + Number(x[20]) + ":" + Number(x[21]) + " @" + Number(x[22]); ret.poweron = Number(x[16]); ret.unknown = Number(x[17]); ret.weekday = getSched(x, 0, 6); ret.weekend = getSched(x, 6, 8); } return ret; } } class LB1 extends Device { constructor(host, mac, devtype, bl) { super(host, mac, devtype, bl); // this.type = "LB1"; LB1.colorMode = { "lovely color": 0, flashlight: 1, lightning: 2, "color fading": 3, "color breathing": 4, "multicolor breathing": 5, "color jumping": 6, "multicolor jumping": 7, }; LB1.colorModeArr = Object.keys(LB1.colorMode); } async _sendCommand(command, type = true /* set = true, query = false */) { // packet = bytearray(16+(int(len(command)/16) + 1)*16) const packet = Buffer.alloc( 16 + (parseInt(command.length / 16) + 1) * 16, 0 ); packet[0x02] = 0xa5; packet[0x03] = 0xa5; packet[0x04] = 0x5a; packet[0x05] = 0x5a; packet[0x08] = type ? 0x02 : 0x01; // # 0x01 => query, # 0x02 => set packet[0x09] = 0x0b; packet[0x0a] = command.length; for (let c = 0; c < command.length; c++) packet[0x0e + c] = command[c].charCodeAt(0); let checksum = 0xbeaf; for (let i = 0; i < packet.length; i++) { checksum += packet[i]; checksum = checksum & 0xffff; } packet[0x00] = (0x0c + command.length) & 0xff; packet[0x06] = checksum & 0xff; // # Checksum 1 position packet[0x07] = checksum >> 8; // # Checksum 2 position // A.I(`Send Command: ${packet.toString('hex')}`); const res = await this.checkOff(this.sendPacket, 0x6a, packet); const ret = { payload: res, }; const e = this.checkError(ret, 0x36); if (e && res) res.err = e; return res; } retVal(res) { let ret = this._val; let payload = res.payload; const len = payload[0xa] + payload[0xb] * 256; if (len > 0) payload = payload.slice(0xe, len + 0xe); else payload = ""; payload = payload.toString(); ret.here = true; ret = Object.assign(ret, JSON.par