Compare commits

...

62 commits

Author SHA1 Message Date
Todd Treece
3623b7335e export client 2016-06-14 17:20:11 -04:00
Todd Treece
2920e220e3 allow authorizations to be passed in via config to client 2016-06-14 14:54:37 -04:00
Todd Treece
b7946a9e4d Merge branch 'api_v2' of github.com:adafruit/io-client-node into api_v2 2016-06-14 11:42:43 -04:00
Todd Treece
f894887452 fix merge error 2016-06-14 11:39:47 -04:00
Todd Treece
7ed74a8804 add swagger client operationId override 2016-06-14 11:39:00 -04:00
Todd Treece
a8cf50c7b4 4.4.0 2016-06-14 11:39:00 -04:00
Todd Treece
874b7ade9f 4.3.0 2016-06-14 11:39:00 -04:00
Todd Treece
aaa4035e7e add user agent to client headers 2016-06-14 11:39:00 -04:00
Todd Treece
700d602701 4.2.7 2016-06-14 11:37:59 -04:00
Todd Treece
d4092eb6ef add latest docs 2016-06-14 11:37:59 -04:00
Todd Treece
12ee3ef61b 4.2.6 2016-06-14 11:37:59 -04:00
Todd Treece
bfd7c20426 update api docs path 2016-06-14 11:37:58 -04:00
Todd Treece
2a40b1444c switch to swagger-client promises 2016-06-14 11:37:58 -04:00
Todd Treece
9d1a189c92 working test version of light accessory 2016-06-14 11:36:33 -04:00
Todd Treece
83e14d02b7 adds node-persist init to homekit cli 2016-06-14 11:36:33 -04:00
Todd Treece
a1fd20c23c adds homekit to gulp syntax check 2016-06-14 11:36:33 -04:00
Todd Treece
536df6bd1e adds accessory index 2016-06-14 11:36:33 -04:00
Todd Treece
0176c93acf adds homekit to main cli 2016-06-14 11:36:33 -04:00
Todd Treece
a92e04d218 adds homekit light accessory 2016-06-14 11:36:33 -04:00
Todd Treece
c9ecab57be adds homekit cli 2016-06-14 11:36:33 -04:00
Todd Treece
d10cabb6e3 adds hap-node dependency 2016-06-14 11:36:33 -04:00
Todd Treece
58430f29f1 4.2.5 2016-06-14 11:36:33 -04:00
Todd Treece
18399e27a8 fix client write stream errors on dropped connection 2016-06-14 11:36:33 -04:00
Todd Treece
8eeb1d0c7a 4.2.4 2016-06-14 11:36:33 -04:00
Todd Treece
b1319313c7 watch for client mqtt reconnect events 2016-06-14 11:36:33 -04:00
Todd Treece
a7ec84f7c4 4.2.3 2016-06-14 11:36:33 -04:00
Todd Treece
c0812e1144 fix yargs issue 2016-06-14 11:36:32 -04:00
Todd Treece
a21c9a206f 4.2.2 2016-06-14 11:36:32 -04:00
Todd Treece
50a5af1213 expose yargs to cli sub modules 2016-06-14 11:36:32 -04:00
Todd Treece
e8d3dd6954 4.2.1 2016-06-14 11:36:32 -04:00
Todd Treece
ed4e97f0a3 fix cli sub module scope issue 2016-06-14 11:36:32 -04:00
Todd Treece
1f52fe01f9 4.2.0 2016-06-14 11:36:32 -04:00
Todd Treece
d562b34890 allow sub commands to be set externally in cli 2016-06-14 11:36:32 -04:00
Todd Treece
14022293b4 4.1.4 2016-06-14 11:36:32 -04:00
Todd Treece
6bde7147a9 adds cli to exported modules 2016-06-14 11:36:32 -04:00
Todd Treece
2da9904981 4.1.3 2016-06-14 11:36:32 -04:00
Todd Treece
fbfa2201db 4.1.2 2016-06-14 11:36:32 -04:00
Todd Treece
c7272a64ed removes console.log 2016-06-14 11:36:32 -04:00
Todd Treece
9b6e8d9787 4.1.1 2016-06-14 11:36:32 -04:00
Todd Treece
e7b47c384e bump highwatermark to 100k for client streams 2016-06-14 11:36:32 -04:00
Todd Treece
bad04cef59 4.1.0 2016-06-14 11:36:32 -04:00
Todd Treece
1134c5985b adds client stdin stream to cli 2016-06-14 11:36:31 -04:00
Todd Treece
f0551dff3b 4.0.6 2016-06-14 11:36:31 -04:00
Todd Treece
abb1e80a43 check to make sure cli tunnel command exists 2016-06-14 11:36:31 -04:00
Todd Treece
9b0997b8fc check to make sure cli server command exists 2016-06-14 11:36:31 -04:00
Todd Treece
3c2f1c9217 fixes cli module export 2016-06-14 11:36:31 -04:00
Todd Treece
e18d38e7d6 fixes nodejs.org link in readme 2016-06-14 11:36:31 -04:00
Todd Treece
0e13b8aef1 add swagger client operationId override 2016-06-14 11:31:38 -04:00
Todd Treece
bd8745a7be Merge branch 'homekit' of github.com:adafruit/io-client-node into api_v2 2016-06-13 16:07:23 -04:00
Todd Treece
fb7303eacb switch to swagger-client promises 2016-01-28 16:40:10 -05:00
Todd Treece
62cc18a3cd working test version of light accessory 2016-01-28 16:01:11 -05:00
Todd Treece
bb4c4272d5 adds node-persist init to homekit cli 2016-01-28 16:00:03 -05:00
Todd Treece
7500a11bbe adds homekit to gulp syntax check 2016-01-28 15:56:01 -05:00
Todd Treece
f9a07a6abb adds accessory index 2016-01-27 18:53:35 -05:00
Todd Treece
17ef2c2487 adds homekit to main cli 2016-01-27 18:50:57 -05:00
Todd Treece
b826823041 adds homekit light accessory 2016-01-27 18:49:01 -05:00
Todd Treece
efb8bd0286 adds homekit cli 2016-01-27 18:48:49 -05:00
Todd Treece
b0ce189ff0 adds hap-node dependency 2016-01-26 14:29:02 -05:00
Todd Treece
e1f26c365a adds static getters for Client.Stream & Client.Signature 2015-09-25 12:52:52 -04:00
Todd Treece
89074c9848 finished client signature class 2015-09-25 12:50:38 -04:00
Todd Treece
187e7c5fe4 use eslint instead of jshint 2015-09-25 12:50:02 -04:00
Todd Treece
ec6eb8a631 adds signature module 2015-09-24 18:36:49 -04:00
11 changed files with 447 additions and 49 deletions

View file

@ -22,21 +22,23 @@ class ClientCLI extends CLI {
init() { init() {
let options = {};
if(! process.env.AIO_CLIENT_USER || ! process.env.AIO_CLIENT_KEY) if(! process.env.AIO_CLIENT_USER || ! process.env.AIO_CLIENT_KEY)
return this.requireAuth(this.yargs); return this.requireAuth(this.yargs);
const options = {
success: this.setupAPI.bind(this, this.yargs),
failure: this.error.bind(this)
};
if(process.env.AIO_CLIENT_HOST) if(process.env.AIO_CLIENT_HOST)
options.host = process.env.AIO_CLIENT_HOST; options.host = process.env.AIO_CLIENT_HOST;
if(process.env.AIO_CLIENT_PORT) if(process.env.AIO_CLIENT_PORT)
options.port = process.env.AIO_CLIENT_PORT; options.port = process.env.AIO_CLIENT_PORT;
this.client = new Client(process.env.AIO_CLIENT_USER, process.env.AIO_CLIENT_KEY, options); new Client(process.env.AIO_CLIENT_USER, process.env.AIO_CLIENT_KEY, options)
.then((client) => {
this.client = client;
this.setupAPI(this.yargs);
})
.catch((err) => this.error(JSON.stringify(err)));
} }
@ -68,10 +70,12 @@ class ClientCLI extends CLI {
}; };
this.client = new Client(process.env.AIO_CLIENT_USER, process.env.AIO_CLIENT_KEY, { new Client(process.env.AIO_CLIENT_USER, process.env.AIO_CLIENT_KEY)
success: parse.bind(this), .then((client) => {
failure: this.error.bind(this) this.client = client;
}); parse();
})
.catch(this.error.bind(this));
} }
@ -196,7 +200,14 @@ class ClientCLI extends CLI {
this.info('Success'); this.info('Success');
console.log(res.obj); console.log(res.obj);
}) })
.catch(res => this.error(res.obj.toString().replace('Error: ', ''))); .catch(res => {
if(argv.json)
return console.log(res.statusText);
this.error(JSON.parse(res.statusText).error);
});
} }
const questions = Object.keys(body.schema.properties).map(name => { const questions = Object.keys(body.schema.properties).map(name => {
@ -219,7 +230,14 @@ class ClientCLI extends CLI {
this.info('Success'); this.info('Success');
console.log(res.obj); console.log(res.obj);
}) })
.catch(res => this.error(res.obj.toString().replace('Error: ', ''))); .catch(res => {
if(argv.json)
return console.log(res.statusText);
this.error(JSON.parse(res.statusText).error);
});
}); });
} }

132
cli/homekit.js Normal file
View file

@ -0,0 +1,132 @@
'use strict';
const Client = require('../client'),
CLI = require('./index'),
Yargs = require('yargs'),
uuid = require('hap-nodejs').uuid,
storage = require('node-persist'),
Bridge = require('hap-nodejs').Bridge,
Accessory = require('hap-nodejs').Accessory,
inquirer = require('inquirer'),
Accessories = require('../homekit'),
crypto = require('crypto');
storage.initSync();
class HomekitCLI extends CLI {
constructor() {
super('homekit');
this.completions = [
'help',
'light'
];
this.yargs = Yargs(process.argv.slice(3));
this.client = false;
}
init() {
if(! process.env.AIO_CLIENT_USER || ! process.env.AIO_CLIENT_KEY)
return this.requireAuth(this.yargs);
const options = {
success: this.clientReady.bind(this, this.yargs),
failure: this.error.bind(this)
};
if(process.env.AIO_CLIENT_HOST)
options.host = process.env.AIO_CLIENT_HOST;
if(process.env.AIO_CLIENT_PORT)
options.port = process.env.AIO_CLIENT_PORT;
this.client = new Client(process.env.AIO_CLIENT_USER, process.env.AIO_CLIENT_KEY, options);
}
clientReady(yargs) {
const argv = yargs
.command('light', 'Light')
.command('help', 'Show help')
.demand(1, 'You must supply a valid homekit command')
.argv;
if(! argv)
return;
const command = argv._[0];
if(command === 'help')
return yargs.showHelp();
if(! this[command])
return yargs.showHelp();
this[command](Yargs(process.argv.slice(4)));
}
light(yargs) {
yargs.usage(`Usage: adafruit-io homekit light [options]`)
.command('help', 'Show help')
.alias('n', 'name').demand('name')
.nargs('n', 1).describe('n', 'the name of the light');
const argv = yargs.argv,
command = argv._[0];
if(command === 'help')
return yargs.showHelp();
const name = argv.name || 'light',
bridge = new Bridge('Adafruit IO', uuid.generate('Adafruit IO'));
bridge.on('identify', (paired, cb) => cb());
const light = new Accessories.light(name, this.client);
bridge.addBridgedAccessory(light);
this.logo();
this.info('advertising homekit light accessory...');
this.info('PIN: 100-11-100');
bridge.publish({
username: 'AD:A0:AD:A0:AD:A0',
port: 60000,
pincode: '100-11-100',
category: Accessory.Categories.BRIDGE
});
}
requireAuth(yargs) {
const argv = yargs
.usage('Usage: adafruit-io homekit config [options]')
.alias('h', 'host').nargs('h', 1).default('h', process.env.AIO_CLIENT_HOST || 'io.adafruit.com').describe('h', 'Server hostname')
.alias('p', 'port').nargs('p', 1).default('p', process.env.AIO_CLIENT_PORT || '80').describe('p', 'Server port')
.alias('u', 'username').demand('username').nargs('u', 1).describe('u', 'Adafruit IO Username')
.alias('k', 'key').demand('key').nargs('k', 1).describe('k', 'Adafruit IO Key')
.command('help', 'Show help')
.argv;
process.env.AIO_CLIENT_HOST = argv.host;
process.env.AIO_CLIENT_PORT = argv.port;
process.env.AIO_CLIENT_USER = argv.username;
process.env.AIO_CLIENT_KEY = argv.key;
this.saveEnv();
}
}
exports = module.exports = HomekitCLI;

View file

@ -16,6 +16,7 @@ class CLI {
this.type = type || 'cli'; this.type = type || 'cli';
this.completions = [ this.completions = [
'client', 'client',
'homekit',
'server', 'server',
'tunnel', 'tunnel',
'help', 'help',
@ -24,6 +25,7 @@ class CLI {
this.sub = { this.sub = {
client: require('./client'), client: require('./client'),
homekit: require('./homekit'),
server: require('./server'), server: require('./server'),
tunnel: require('./tunnel') tunnel: require('./tunnel')
}; };
@ -50,6 +52,7 @@ class CLI {
this.yargs this.yargs
.usage('Usage: adafruit-io <command>') .usage('Usage: adafruit-io <command>')
.command('server', 'Adafruit IO local server') .command('server', 'Adafruit IO local server')
.command('homekit', 'Adafruit IO homekit bridge')
.command('client', 'Adafruit IO client') .command('client', 'Adafruit IO client')
.command('tunnel', 'TLS tunnel to io.adafruit.com') .command('tunnel', 'TLS tunnel to io.adafruit.com')
.command('help', 'Show help') .command('help', 'Show help')
@ -163,7 +166,7 @@ class CLI {
if(Object.keys(children).indexOf(commands[0]) < 0) if(Object.keys(children).indexOf(commands[0]) < 0)
return done([]); return done([]);
if(commands[0] === 'server' || commands[0] === 'tunnel') { if(commands[0] === 'server' || commands[0] === 'tunnel' || commands[0] === 'homekit') {
const child = new children[commands[0]](); const child = new children[commands[0]]();

View file

@ -1,7 +1,8 @@
'use strict'; 'use strict';
const Swagger = require('swagger-client-promises'), const Swagger = require('./lib/client'),
Stream = require('./lib/stream'), Stream = require('./lib/stream'),
Signature = require('./lib/signature'),
pkg = require('../package.json'); pkg = require('../package.json');
class HeaderKey { class HeaderKey {
@ -26,11 +27,12 @@ class Client {
this.port = 80; this.port = 80;
this.username = username || false; this.username = username || false;
this.key = key || false; this.key = key || false;
this.swagger_path = '/api/docs/v1.json'; this.swagger_path = '/api/docs/v2.json';
this.success = function() {}; this.authorizations = {
this.failure = function(err) { throw err; }; HeaderKey: new HeaderKey(this.key)
};
Object.assign(this, options); Object.assign(this, options || {});
if(! this.username) if(! this.username)
throw new Error('client username is required'); throw new Error('client username is required');
@ -38,14 +40,14 @@ class Client {
if(! this.key) if(! this.key)
throw new Error('client key is required'); throw new Error('client key is required');
this.swagger = new Swagger({ return new Swagger({
url: `http://${this.host}:${this.port}${this.swagger_path}`, url: `http://${this.host}:${this.port}${this.swagger_path}`,
debug: true, usePromise: true,
success: this._defineGetters.bind(this), authorizations: this.authorizations
failure: this.failure, }).then((client) => {
authorizations: { this.swagger = client;
HeaderKey: new HeaderKey(this.key) this._defineGetters();
} return this;
}); });
} }
@ -68,6 +70,7 @@ class Client {
this.swagger[api].readable = (id) => { stream.connect(id); return stream; }; this.swagger[api].readable = (id) => { stream.connect(id); return stream; };
this.swagger[api].writable = (id) => { stream.connect(id); return stream; }; this.swagger[api].writable = (id) => { stream.connect(id); return stream; };
// add dynamic getter to this class for the API
Object.defineProperty(this, api, { Object.defineProperty(this, api, {
get: () => { get: () => {
return this.swagger[api]; return this.swagger[api];
@ -76,8 +79,14 @@ class Client {
}); });
this.success(); }
static get Signature() {
return Signature;
}
static get Stream() {
return Stream;
} }
} }

12
client/lib/client.js Normal file
View file

@ -0,0 +1,12 @@
const Swagger = require('swagger-client');
class Client extends Swagger {
idFromOp(path, httpMethod, op) {
op.operationId = op['x-swagger-router-action'];
return super.idFromOp(path, httpMethod, op);
}
}
exports = module.exports = Client;

99
client/lib/signature.js Normal file
View file

@ -0,0 +1,99 @@
'use strict';
const crypto = require('crypto'),
url = require('url');
class Signature {
constructor(options) {
this.username = false;
this.key = false;
this.host = false;
this.method = 'GET';
this.params = false;
this.path = false;
this.parsed = false;
Object.assign(this, options || {});
if(! this.username)
throw new Error('Username is required');
if(! this.key)
throw new Error('AIO Key is required');
if(! this.path)
throw new Error('path is required');
}
toString() {
const step1 = this.hmac(this.key, this.date()),
step2 = this.hmac(step1, this.host),
step3 = this.hmac(step2, this.method),
step4 = this.hmac(step3, this.params);
const signing_key = this.hmac(step4, this.version),
canonical_request = this.hash(this.request),
to_sign = `${this.algorithm}\n${this.date}\n${canonical_request}`;
return this.hmac(signing_key, to_sign);
}
get request() {
return `${this.method}\n${this.path}?${this.params}
host: ${this.host}
x-aio-date: ${this.date}`;
}
get version() {
return 'aio-signature-v1';
}
get credential() {
return `${this.username}/${this.version}`;
}
get date() {
return (new Date()).toISOString();
}
get algorithm() {
return 'sha512';
}
get algorithmName() {
return `aio-hmac-${this.algorithm()}`;
}
get path() {
return this.path;
}
set path(path) {
this.parsed = url.parse(path, true);
this.params = Object.keys(this.parsed.query).sort().map(q => q.toLowerCase()).join('&');
this.host = this.parsed.host;
this.path = this.parsed.href.split('?')[0];
}
hash(data) {
const hash = crypto.createHash(this.algorithm());
return hmac.update(data).digest('hex');
}
hmac(key, data) {
const hmac = crypto.createHmac(this.algorithm(), key);
return hmac.update(data).digest('hex');
}
}
exports = module.exports = Signature;

View file

@ -1,34 +1,27 @@
'use strict'; 'use strict';
const gulp = require('gulp'), const gulp = require('gulp'),
jshint = require('gulp-jshint'); eslint = require('gulp-eslint');
gulp.task('lint', function() { gulp.task('lint', function() {
const lint = jshint({ const config = {
"esnext": true, ecmaFeatures: {
"curly": false, templateStrings: true
"eqeqeq": true, },
"immed": true, env: ['node', 'es6']
"newcap": false, };
"noarg": true,
"sub": true,
"unused": "var",
"boss": true,
"eqnull": true,
"node": true,
"-W086": true
});
return gulp.src([ return gulp.src([
'index.js', 'index.js',
'cli/*.js', 'cli/*.js',
'homekit/*.js',
'client/**/*.js', 'client/**/*.js',
'server/index.js', 'server/index.js',
'server/lib/*.js', 'server/lib/*.js',
'tunnel/index.js', 'tunnel/index.js',
'tunnel/lib/*.js' 'tunnel/lib/*.js'
]).pipe(lint).pipe(jshint.reporter('jshint-stylish')); ]).pipe(eslint(config)).pipe(eslint.format()).pipe(eslint.failAfterError());
}); });

3
homekit/index.js Normal file
View file

@ -0,0 +1,3 @@
exports = module.exports = {
light: require('./light')
};

129
homekit/light.js Normal file
View file

@ -0,0 +1,129 @@
'use strict';
const Hap = require('hap-nodejs'),
Accessory = Hap.Accessory,
Service = Hap.Service,
Characteristic = Hap.Characteristic,
uuid = Hap.uuid;
class Light extends Accessory {
constructor(name, io) {
super(name, uuid.generate(`adafruit:accessories:light-${name}`));
this.getService(Service.AccessoryInformation)
.setCharacteristic(Characteristic.Manufacturer, 'Adafruit Industries')
.setCharacteristic(Characteristic.Model, 'Adafruit IO Light')
.setCharacteristic(Characteristic.SerialNumber, 'AIOLIGHT01');
// just call cb on ident
this.on('identify', (paired, cb) => cb());
this._name = name;
this._io = io;
this._state = {
power: 0,
brightness: 100,
hue: 255,
saturation: 100
};
this._stream = this._io.Groups.writable(this._name);
let timer = setTimeout(() => {
this._writeState();
}, 1000);
this._stream.on('data', (group) => {
if(timer) {
clearTimeout(timer);
timer = false;
}
group = JSON.parse(group.toString());
Object.keys(group.feeds).forEach((key) => {
if(/^brightness/.test(key))
this._state.brightness = parseInt(group.feeds[key]);
if(/^power/.test(key))
this._state.power = parseInt(group.feeds[key]);
if(/^hue/.test(key))
this._state.hue = parseInt(group.feeds[key]);
if(/^saturation/.test(key))
this._state.saturation = parseInt(group.feeds[key]);
});
});
const service = this.addService(Service.Lightbulb, `Adafruit Light`);
service.getCharacteristic(Characteristic.On)
.on('set', this._lightOnSet.bind(this))
.on('get', this._lightOnGet.bind(this));
service.getCharacteristic(Characteristic.Brightness)
.on('set', this._lightBrightnessSet.bind(this))
.on('get', this._lightBrightnessGet.bind(this));
service.getCharacteristic(Characteristic.Hue)
.on('set', this._lightHueSet.bind(this))
.on('get', this._lightHueGet.bind(this));
service.getCharacteristic(Characteristic.Saturation)
.on('set', this._lightSaturationSet.bind(this))
.on('get', this._lightSaturationGet.bind(this));
}
_lightOnGet(cb) {
cb(null, this._state.power);
}
_lightOnSet(value, cb) {
this._state.power = value ? 1 : 0;
this._writeState();
cb();
}
_lightBrightnessGet(cb) {
cb(null, this._state.brightness);
}
_lightBrightnessSet(value, cb) {
this._state.brightness = parseInt(value);
this._writeState();
cb();
}
_lightSaturationGet(cb) {
cb(null, this._state.saturation);
}
_lightSaturationSet(value, cb) {
this._state.saturation = parseInt(value);
this._writeState();
cb();
}
_lightHueGet(cb) {
cb(null, this._state.hue);
}
_lightHueSet(value, cb) {
this._state.hue = parseInt(value);
this._writeState();
cb();
}
_writeState() {
Object.keys(this._state).forEach((key) => {
this._state[key] = this._state[key] || 0;
});
this._stream.write(JSON.stringify({ feeds: this._state }));
}
}
exports = module.exports = Light;

View file

@ -1,3 +1,3 @@
exports = module.exports = require('./client'); exports = {
Client: require('./client')
module.exports.CLI = require('./cli/index'); };

View file

@ -19,9 +19,8 @@
"homepage": "https://github.com/adafruit/adafruit-io-node#readme", "homepage": "https://github.com/adafruit/adafruit-io-node#readme",
"devDependencies": { "devDependencies": {
"gulp": "^3.9.0", "gulp": "^3.9.0",
"gulp-jshint": "^1.11.2", "gulp-eslint": "^1.0.0",
"gulp-mocha": "^2.1.3", "gulp-mocha": "^2.1.3"
"jshint-stylish": "^2.0.1"
}, },
"keywords": [ "keywords": [
"adafruit", "adafruit",
@ -45,12 +44,13 @@
"dotenv": "^1.2.0", "dotenv": "^1.2.0",
"express": "^4.13.3", "express": "^4.13.3",
"express-csv": "^0.6.0", "express-csv": "^0.6.0",
"hap-nodejs": "^0.3.2",
"inquirer": "^0.10.0", "inquirer": "^0.10.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mqtt": "^1.4.1", "mqtt": "^1.4.1",
"nedb": "^1.1.3", "nedb": "^1.1.3",
"restler": "^3.2.2", "restler": "^3.2.2",
"swagger-client-promises": "^1.0.2", "swagger-client": "^2.1.14",
"swagger-tools": "^0.9.5", "swagger-tools": "^0.9.5",
"winston": "^1.0.1", "winston": "^1.0.1",
"xml": "^1.0.0", "xml": "^1.0.0",