diff --git a/src/devices/base.js b/src/devices/base.js index 7d05a86..0e18b98 100644 --- a/src/devices/base.js +++ b/src/devices/base.js @@ -15,7 +15,7 @@ class Base { this.api = { system: { get_sysinfo: errCode(() => { - return this.data.system.sysinfo; + return this.sysinfo; }), }, cnCloud: { @@ -41,6 +41,10 @@ class Base { }; } + get sysinfo() { + return this.data.system.sysinfo; + } + get emeterContext() { return this.data.emeter; } diff --git a/src/devices/data/kl.js b/src/devices/data/kl.js new file mode 100644 index 0000000..f4db5cb --- /dev/null +++ b/src/devices/data/kl.js @@ -0,0 +1,71 @@ +const base = require('./base'); + +const kl = { ...base }; +module.exports = kl; + +Object.assign(kl, { + cnCloud: { + info: { + username: '', + server: 'n-devs.tplinkcloud.com', + binded: 0, + cld_connection: 1, + illegalType: 0, + stopConnect: 0, + tcspStatus: 1, + fwDlPage: '', + tcspInfo: '', + fwNotifyType: -1, + err_code: 0, + }, + }, + + emeter: { + realtime: { + power_mw: 10800, + }, + daystat: { + day_list: [], + }, + }, + + 'smartlife.iot.lightStrip': { + fade_on_off: { fadeOnTime: 500, fadeOffTime: 500 }, + + light_state: { + transition: 0, + length: 16, + on_off: 0, + dft_on_state: { mode: 'normal', groups: [[0, 15, 0, 0, 100, 9000]] }, + }, + + dft_on_state: { + mode: 'normal', + hue: 0, + saturation: 0, + color_temp: 9000, + brightness: 100, + groups: [[0, 15, 0, 0, 100, 9000]], + }, + + get_light_details: { + lamp_beam_angle: 270, + min_voltage: 100, + max_voltage: 120, + wattage: 17, + incandescent_equivalent: 100, + max_lumens: 1400, + color_rendering_index: 85, + }, + + get_default_behavior: { + soft_on: { + mode: 'last_status', + }, + hard_on: { + mode: 'last_status', + }, + err_code: 0, + }, + }, +}); diff --git a/src/devices/data/kl430.js b/src/devices/data/kl430.js new file mode 100644 index 0000000..967ef27 --- /dev/null +++ b/src/devices/data/kl430.js @@ -0,0 +1,50 @@ +const defaultsDeep = require('lodash.defaultsdeep'); + +const kl = require('./kl'); + +const kl430 = { + colorTempRange: { min: 2500, max: 9000 }, + + system: { + sysinfo: { + sw_ver: '1.0.9 Build 200305 Rel.090639', + hw_ver: '1.0', + mic_type: 'IOT.SMARTBULB', + model: 'KL430(US)', + deviceId: '801222BAA511374991036875D0A280AD1D35A16F', + oemId: '1A3F21A5B9AE0ED6C80ED1A107885DB2', + hwId: '375D4CCE7C909516CFD57BA93A304404', + description: 'Kasa Smart Light Strip, Multicolor', + length: 16, + is_dimmable: 1, + is_color: 1, + is_variable_color_temp: 1, + + status: 'new', + rssi: -58, + + is_factory: false, + disco_ver: '1.0', + + ctrl_protocols: { + name: 'Linkie', + version: '1.0', + }, + + lighting_effect_state: { + enable: 1, + name: 'Aurora', + custom: 0, + id: 'xqUxDhbAhNLqulcuRMyPBmVGyTOyEMEu', + brightness: 63, + }, + + dev_state: 'normal', + active_mode: 'none', + preferred_state: [], + }, + }, +}; +defaultsDeep(kl430, kl); + +module.exports = kl430; diff --git a/src/devices/hs.js b/src/devices/hs.js index 7d11904..4516f8b 100644 --- a/src/devices/hs.js +++ b/src/devices/hs.js @@ -24,10 +24,7 @@ class Hs extends Base { super(data); defaultsDeep(this.data, defaultData); - this.api.system = { - get_sysinfo: errCode(() => { - return this.sysinfo; - }), + Object.assign(this.api.system, { set_dev_alias: errCode(({ alias }) => { this.alias = alias; }), @@ -92,7 +89,7 @@ class Hs extends Base { set_dev_icon: errCode((data) => { this.data.system.dev_icon = data; }), - }; + }); this.api.cnCloud = { get_info: errCode(() => { diff --git a/src/devices/kl.js b/src/devices/kl.js new file mode 100644 index 0000000..f36bc69 --- /dev/null +++ b/src/devices/kl.js @@ -0,0 +1,151 @@ +/* eslint-disable camelcase */ +const defaultsDeep = require('lodash.defaultsdeep'); + +const { errCode, pick, randomMac } = require('../utils'); + +const Base = require('./base'); +const Hs = require('./hs'); + +const defaultData = require('./data/base'); + +class Kl extends Base { + static get errors() { + return { + MODULE_NOT_SUPPORT: { err_code: -1, err_msg: 'module not support' }, + METHOD_NOT_SUPPORT: { err_code: -2, err_msg: 'member not support' }, + INVALID_ARGUMENT: null, + MISSING_ARGUMENT: { + err_code: -10002, + /* cspell:disable-next-line */ + err_msg: 'Missing neccesary argument', + }, + }; + } + + constructor(data) { + super(data); + this.hs = new Hs(data); + this.data = defaultsDeep(data, defaultData); + + const { get_sysinfo } = this.api.system; + + this.api.system = this.hs.api.system; + this.api.system.get_sysinfo = get_sysinfo; + + this.api['smartlife.iot.common.system'] = this.api.system; + + this.api['smartlife.iot.common.cloud'] = this.hs.api.cnCloud; + + this.api['smartlife.iot.common.schedule'] = this.hs.api.schedule; + + this.api['smartlife.iot.common.timesetting'] = this.hs.api.time; + + this.api['smartlife.iot.common.timesetting'].set_time = errCode( + // eslint-disable-next-line no-unused-vars + ({ year, month, mday, hour, min, sec }) => { + // TODO + } + ); + + this.api.netif = this.hs.api.netif; + + this.api['smartlife.iot.common.emeter'] = { + get_realtime: errCode(() => { + return this.hs.emeterContext.realtime; + }), + get_daystat: this.hs.api.emeter.get_daystat, + get_monthstat: this.hs.api.emeter.get_monthstat, + erase_emeter_stat: this.hs.api.emeter.erase_emeter_stat, + }; + + this.api['smartlife.iot.lightStrip'] = { + get_fade_on_off: errCode(() => { + return this.data['smartlife.iot.lightStrip'].fade_on_off; + }), + + set_light_state: errCode((options) => { + const ls = this.data['smartlife.iot.lightStrip'].light_state; + + Object.entries(options).forEach(([k, v]) => { + switch (k) { + case 'color_temp': + if ( + v === 0 || + (v >= this.data.colorTempRange.min && + v <= this.data.colorTempRange.max) + ) { + Object.assign(ls, { [k]: v }); + } else { + throw { err_code: -10000, err_msg: 'Invalid input argument' }; // eslint-disable-line no-throw-literal + } + break; + case 'mode': + case 'hue': + case 'on_off': + case 'saturation': + case 'brightness': + Object.assign(ls, { [k]: v }); + break; + default: + // do nothing + } + }); + + return pick(ls, ['on_off', 'mode', 'groups']); + }), + + get_light_state: errCode(() => { + return this.data['smartlife.iot.lightStrip'].light_state; + }), + + get_light_details: errCode(() => { + return this.data['smartlife.iot.lightStrip'].get_light_details; + }), + + get_default_behavior: errCode(() => { + return this.data['smartlife.iot.lightStrip'].get_default_behavior; + }), + }; + } + + // eslint-disable-next-line class-methods-use-this + get endSocketAfterResponse() { + return false; + } + + get sysinfo() { + this.data.system.sysinfo.on_time = this.onTime; + + this.data.system.sysinfo.light_state = pick( + this.data['smartlife.iot.lightStrip'].light_state, + ['on_off', 'mode', 'hue', 'saturation', 'color_temp', 'brightness'] + ); + + this.data.system.sysinfo.light_state.dft_on_state = pick( + this.data['smartlife.iot.lightStrip'].dft_on_state, + ['mode', 'hue', 'saturation', 'color_temp', 'brightness'] + ); + + return this.data.system.sysinfo; + } + + get mac() { + return this.data.system.sysinfo.mic_mac; + } + + set mac(value) { + this.data.system.sysinfo.mic_mac = value; + } + + initDefaults() { + super.initDefaults(); + + if (this.data.mac) this.mac = this.data.mac; + + if (!this.mac) { + this.mac = randomMac(); + } + } +} + +module.exports = Kl; diff --git a/src/devices/kl430.js b/src/devices/kl430.js new file mode 100644 index 0000000..b4e2e33 --- /dev/null +++ b/src/devices/kl430.js @@ -0,0 +1,15 @@ +const defaultsDeep = require('lodash.defaultsdeep'); + +const Kl = require('./kl'); + +const defaultData = require('./data/kl430'); + +class Kl430 extends Kl { + constructor(data) { + super(data); + this.data.cnCloud = defaultData.cnCloud; + defaultsDeep(this.data, defaultData); + } +} + +module.exports = Kl430; diff --git a/src/utils.js b/src/utils.js index d55ed49..7582132 100644 --- a/src/utils.js +++ b/src/utils.js @@ -52,6 +52,16 @@ function generateId(len) { .toUpperCase(); } +function pick(object, keys) { + return keys.reduce((obj, key) => { + if (object && Object.prototype.hasOwnProperty.call(object, key)) { + // eslint-disable-next-line no-param-reassign + obj[key] = object[key]; + } + return obj; + }, {}); +} + function parseJsonStream(json) { const parser = new Parser(); const results = []; @@ -114,11 +124,9 @@ function processCommands(json, api, errors, customizerFn) { if (module.name === 'context' && !foundFirstContext) { foundFirstContext = true; if ('context' in api && 'child_ids' in api.context) { - const childIds = module.methods.find( - (v) => v.name === 'child_ids' - ).args; - if (childIds !== undefined) { - api.context.child_ids(childIds); + const childIds = module.methods.find((v) => v.name === 'child_ids'); + if (childIds !== undefined && childIds.args !== undefined) { + api.context.child_ids(childIds.args); } } } else { @@ -285,6 +293,7 @@ module.exports = { randomMac, generateId, parseJsonStream, + pick, processCommands, randomInt, randomFloat, diff --git a/test/device.js b/test/device.js index 6656911..31c40db 100644 --- a/test/device.js +++ b/test/device.js @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ /* eslint-env mocha */ /* eslint no-unused-expressions: ["off"] */ @@ -17,23 +18,46 @@ describe('Device', function () { this.retries(2); describe('constructor()', function () { - it('accept options', function () { - const opt = { - model: 'hs100', - port: 1234, - address: '127.0.0.1', - data: { deviceId: 'ABC' }, - }; - const device = new Device(opt); - expect(device).to.have.property('model', opt.model); - expect(device).to.have.property('port', opt.port); - expect(device).to.have.property('address', opt.address); - expect(device).to.have.nested.property( - 'data.deviceId', - opt.data.deviceId - ); - expect(device.api).to.exist; + describe('arguments', function () { + const opts = [ + { + model: 'hs100', + port: 1234, + address: '127.0.0.1', + alias: 'MY ALIAS', + data: { deviceId: 'ABC', mac: 'aa:aa:aa:bb:bb:bb' }, + }, + ]; + + Device.models.forEach(function (model) { + describe(model, function () { + opts.forEach(function (opt) { + let device; + before(function () { + device = new Device({ ...opt, model }); + }); + it('accept options', function () { + expect(device).to.have.property('model', model); + expect(device).to.have.property('port', opt.port); + expect(device).to.have.property('address', opt.address); + expect(device).to.have.nested.property('data.alias', opt.alias); + expect(device).to.have.nested.property( + 'data.deviceId', + opt.data.deviceId + ); + expect(device).to.have.nested.property('data.mac', opt.data.mac); + expect(device.api).to.exist; + }); + + it('sets mac', function () { + if (opt.data.mac === undefined) this.skip(); + expect(device._deviceInfo.mac).to.eql(opt.data.mac); + }); + }); + }); + }); }); + it('defaults', function () { const device = new Device({ model: 'hs100' }); expect(device).to.have.property('port', 0);