From 8ec64ce2a0ad9bbc487c2e070d70f2652efd6711 Mon Sep 17 00:00:00 2001 From: Andreas Holstenson Date: Sun, 19 Mar 2017 18:50:49 +0100 Subject: [PATCH] First version of Miio library --- .editorconfig | 15 ++++++++ .eslintrc | 20 ++++++++++ .gitignore | 2 + example.js | 18 +++++++++ index.js | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 23 ++++++++++++ packet.js | 83 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 261 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 example.js create mode 100644 index.js create mode 100644 package.json create mode 100644 packet.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..abbfdfe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[package.json] +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..8168fbf --- /dev/null +++ b/.eslintrc @@ -0,0 +1,20 @@ +{ + "plugins": [ "node" ], + "extends": [ "eslint:recommended", "plugin:node/recommended" ], + "env": {}, + "ecmaFeatures": {}, + "globals": {}, + "rules": { + "node/no-unsupported-features": [ + "error", + { + "version": 6 + } + ], + "no-irregular-whitespace": 2, + "quotes": [ + 2, + "single" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93f1361 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/example.js b/example.js new file mode 100644 index 0000000..8cd0cff --- /dev/null +++ b/example.js @@ -0,0 +1,18 @@ +'use strict'; + +const { Device } = require('./index'); + +// Create a new device over the given address +const device = new Device('192.168.100.8'); + +// Call any method via call +device.call('set_mode', [ 'silent' ]) + .then(console.log) + .catch(console.error); + +// Or use some of the built-in methods +device.setPower(false); + +device.getProperties([ 'power', 'mode', 'aqi', 'temp_dec', 'humidity' ]) + .then(console.log) + .catch(console.error); diff --git a/index.js b/index.js new file mode 100644 index 0000000..3fa15e1 --- /dev/null +++ b/index.js @@ -0,0 +1,100 @@ +'use strict'; + +const dgram = require('dgram'); +const Packet = require('./packet'); + +class Device { + constructor(address) { + this.address = address; + + this.packet = new Packet(); + this.socket = dgram.createSocket('udp4'); + this.socket.on('message', this._onMessage.bind(this)); + + this._id = 0; + this._promises = {}; + } + + _onMessage(msg) { + this.packet.raw = msg; + + if(this._tokenResolve) { + this.packet.token = this.packet.checksum; + this._tokenResolve(); + this._lastToken = Date.now(); + this._tokenResolve = null; + this._tokenPromise = null; + } else { + let str = this.packet.data.toString('utf8'); + let object = JSON.parse(str); + + const p = this._promises[object.id]; + if(! p) return; + if(object.result) { + p.resolve(object.result); + } else { + p.reject(object.error); + } + + delete this._promises[object.id]; + } + } + + _ensureToken() { + if(this._lastToken > Date.now() - 60000) { + return Promise.resolve(); + } + + if(this._tokenPromise) { + return this._tokenPromise; + } + + this._tokenPromise = new Promise((resolve, reject) => { + const data = this.packet.raw; + this.socket.send(data, 0, data.length, 54321, this.address, err => err && reject(err)); + this._tokenResolve = resolve; + }); + return this._tokenPromise; + } + + call(method, params) { + return this._ensureToken() + .then(() => { + this._id = this._id == 10000 ? 1 : this._id + 1; + this.packet.data = Buffer.from(JSON.stringify({ + id: this._id, + method: method, + params: params + }), 'utf8'); + + const data = this.packet.raw; + + return new Promise((resolve, reject) => { + this._promises[this._id] = { + resolve: resolve, + reject: reject + }; + + this.socket.send(data, 0, data.length, 54321, this.address, err => err && reject(err)); + }); + }); + } + + setPower(on) { + return this.call('set_power', [ on ? 'on' : 'off ']) + .then(() => on); + } + + getProperties(props) { + return this.call('get_prop', props) + .then(result => { + const obj = {}; + for(let i=0; i 0) { + let decipher = crypto.createDecipheriv('aes-128-cbc', this._tokenKey, this._tokenIV); + this.data = Buffer.concat([ + decipher.update(encrypted), + decipher.final() + ]); + } + } + + get token() { + return this._token; + } + + set token(t) { + this._token = t; + this._tokenKey = crypto.createHash('md5').update(t).digest(); + this._tokenIV = crypto.createHash('md5').update(this._tokenKey).update(t).digest(); + } + + get checksum() { + return this.header.slice(16); + } +} + +module.exports = Packet;