UNPKG

tplink-lightbulb-fix

Version:

Control TP-Link smart-home devices from nodejs

233 lines (206 loc) 6.44 kB
import dgram from 'dgram' import EventEmitter from 'events' export default class TPLSmartDevice { constructor (ip) { this.ip = ip } // Scan for lightbulbs on your network static scan (filter, broadcast = '255.255.255.255') { const emitter = new EventEmitter() const client = dgram.createSocket({ type: 'udp4', reuseAddr: true }) client.bind(9998, undefined, () => { client.setBroadcast(true) const msgBuf = TPLSmartDevice.encrypt(Buffer.from('{"system":{"get_sysinfo":{}}}')) client.send(msgBuf, 0, msgBuf.length, 9999, broadcast) }) client.on('message', (msg, rinfo) => { const decryptedMsg = this.decrypt(msg).toString('ascii') const jsonMsg = JSON.parse(decryptedMsg) const sysinfo = jsonMsg.system.get_sysinfo if (filter && sysinfo.mic_type !== filter) { return } const light = new TPLSmartDevice(rinfo.address) light._info = rinfo light._sysinfo = sysinfo light.host = rinfo.address light.port = rinfo.port light.name = sysinfo.alias light.deviceId = sysinfo.deviceId emitter.emit('light', light) }) emitter.stop = () => client.close() return emitter } // Send a message to a lightbulb (for RAW JS message objects) send (msg) { return new Promise((resolve, reject) => { if (!this.ip) { return reject(new Error('IP not set.')) } const client = dgram.createSocket('udp4') const message = this.encrypt(Buffer.from(JSON.stringify(msg))) client.send(message, 0, message.length, 9999, this.ip, (err, bytes) => { if (err) { return reject(err) } client.on('message', msg => { resolve(JSON.parse(this.decrypt(msg).toString())) client.close() }) }) }) } // TODO: wifi needs more testing. it seems very broken. // Scans the wifi networks in range of the device async listwifi () { const r1 = await this.send({ netif: { get_scaninfo: { refresh: 1 } } }) if (r1?.netif?.get_scaninfo?.ap_list) { return r1.netif.get_scaninfo.ap_list } else { // on fail, try older message-format const r2 = await this.send({ 'smartlife.iot.common.softaponboarding': { get_scaninfo: { refresh: 1 } } }) if (r2 && r2['smartlife.iot.common.softaponboarding']?.get_scaninfo?.ap_list) { return r2['smartlife.iot.common.softaponboarding'].get_scaninfo.ap_list } } } // Connects the device to the access point in the parameters async connectwifi (ssid, password, keyType = 1, cypherType = 0) { const r1 = await this.send({ netif: { set_stainfo: { cypher_type: cypherType, key_type: keyType, password, ssid } } }) if (r1?.netif?.set_stainfo?.err_code === 0) { return true } // on fail, try older message-format const r2 = await this.send({ 'smartlife.iot.common.softaponboarding': { set_stainfo: { cypher_type: cypherType, key_type: keyType, password, ssid } } }) if (r2['smartlife.iot.common.softaponboarding'] && r2['smartlife.iot.common.softaponboarding'].err_msg) { throw new Error(r2['smartlife.iot.common.softaponboarding'].err_msg) } else { return true } } // Get info about the TPLSmartDevice async info () { const r = await this.send({ system: { get_sysinfo: {} } }) return r.system.get_sysinfo } // Set power-state of lightbulb async power (powerState = true, transition = 0, options = {}) { const info = await this.info() if (typeof info.relay_state !== 'undefined') { return this.send({ system: { set_relay_state: { state: powerState ? 1 : 0 } } }) } else { const r = await this.send({ 'smartlife.iot.smartbulb.lightingservice': { transition_light_state: { ignore_default: 1, on_off: powerState ? 1 : 0, transition_period: transition, ...options } } }) return r['smartlife.iot.smartbulb.lightingservice'].transition_light_state } } // Set led-state of lightbulb led (ledState = true) { return this.send({ system: { set_led_off: { off: ledState ? 0 : 1 } } }) } // Set the name of lightbulb async name (newAlias) { const info = await this.info() return typeof info.dev_name !== 'undefined' ? this.send({ system: { set_dev_alias: { alias: newAlias } } }) : this.send({ 'smartlife.iot.common.system': { set_dev_alias: { alias: newAlias } } }) } // Get schedule info async daystat (month, year) { const now = new Date() month = month || now.getMonth() + 1 year = year || now.getFullYear() const r = await this.send({ 'smartlife.iot.common.schedule': { get_daystat: { month: month, year: year } } }) return r['smartlife.iot.common.schedule'].get_daystat } // Get cloud info from bulb async cloud () { const r = await this.send({ 'smartlife.iot.common.cloud': { get_info: {} } }) return r['smartlife.iot.common.cloud'].get_info } // Get schedule from bulb async schedule () { const r = await this.send({ 'smartlife.iot.common.schedule': { get_rules: {} } }) return r['smartlife.iot.common.schedule'].get_rules } // Get operational details from bulb details () { return this.send({ 'smartlife.iot.smartbulb.lightingservice': { get_light_details: {} } }) } // Reboot the device reboot () { return this.send({ 'smartlife.iot.common.system': { reboot: { delay: 1 } } }) } // Badly encrypt message in format bulbs use static encrypt (buffer, key = 0xAB) { for (let i = 0; i < buffer.length; i++) { const c = buffer[i] buffer[i] = c ^ key key = buffer[i] } return buffer } encrypt (buffer, key) { return TPLSmartDevice.encrypt(buffer, key) } // Badly decrypt message from format bulbs use static decrypt (buffer, key = 0xAB) { for (let i = 0; i < buffer.length; i++) { const c = buffer[i] buffer[i] = c ^ key key = c } return buffer } decrypt (buffer, key) { return TPLSmartDevice.decrypt(buffer, key) } }