Skip to content

Commit

Permalink
Adding miio.devices to discover and connect to devices and sub-devices
Browse files Browse the repository at this point in the history
  • Loading branch information
aholstenson committed Apr 27, 2017
1 parent db2625e commit d7789b5
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 52 deletions.
53 changes: 47 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,56 @@ and common use cases.

## Discovering devices

Use `miio.browser()` to look for devices on the local network. This method of
discovery will tell you directly if a device reveals its token and can be
auto-connected to. It will not tell you the model of devices until they are
connected to via `miio.device()`.
Use `miio.devices()` to look for and connect to devices on the local network.
This method of discovery will tell you directly if a device reveals its token
and can be auto-connected to. If you do not want to automatically connect to
devices you can use `miio.browse()` instead.

Example:
Example using `miio.devices()`:

```javascript
const browser = miio.browser({
const devices = miio.devices({
cacheTime: 300 // 5 minutes. Default is 1800 seconds (30 minutes)
});

browser.on('available', reg => {
if(! reg.token) {
console.log(reg.id, 'hides its token');
return;
}

const device = reg.device;
if(! device) {
console.log(reg.id, 'could not be connected to');
return;
}

// Do something useful with the device
});

browser.on('unavailable', reg => {
if(! reg.device) return;

// Do whatever you need here
});

browser.on('error', err => {
// err.device points to info about the device
console.log('Something went wrong connecting to device', err);
});
```

`miio.devices()` supports these options:

* `cacheTime`, the maximum amount of seconds a device can be unreachable before it becomes unavailable. Default: `1800`
* `useTokenStorage`, if tokens should be fetched from storage (see device management). Default: `true`
* `filter`, function used to filter what devices are connected to. Default: `reg => true`
* `skipSubDevices`, if sub devices on Aqara gateways should be skipped. Default: `false`

Example using `miio.browse()`:

```javascript
const browser = miio.browse({
cacheTime: 300 // 5 minutes. Default is 1800 seconds (30 minutes)
});

Expand Down
15 changes: 11 additions & 4 deletions cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const chalk = require('chalk');

const Packet = require('../lib/packet');
const Device = require('../lib/device');
const { Browser } = require('../lib/discovery');
const { Browser, Devices } = require('../lib/discovery');
const Tokens = require('../lib/tokens');
const models = require('../lib/models');

Expand All @@ -32,19 +32,26 @@ function log() {
if(args.discover) {
info('Discovering devices. Press Ctrl+C to stop.')
log();
const browser = new Browser({
const browser = new Devices({
cacheTime: 60,
useTokenStorage: true
});
browser.on('available', reg => {
const supported = reg.model && reg.type;
log(chalk.bold('Device ID:'), reg.id);
log(chalk.bold('Model info:'), reg.model || 'Unknown', reg.type ? chalk.dim('(' + reg.type + ')') : '');
log(chalk.bold('Address:'), reg.address, (reg.hostname ? chalk.dim('(' + reg.hostname + ')') : ''));

if(reg.address) {
log(chalk.bold('Address:'), reg.address, (reg.hostname ? chalk.dim('(' + reg.hostname + ')') : ''));
} else if(reg.parent) {
log(chalk.bold('Address:'), 'Owned by', reg.parent.id);
}
if(reg.token) {
log(chalk.bold('Token:'), reg.token, reg.autoToken ? chalk.green('via auto-token') : chalk.yellow('via stored token'));
} else {
} else if(! reg.parent) {
log(chalk.bold('Token:'), '???')
} else {
log(chalk.bold('Token:'), chalk.green('Automatic via parent device'));
}
log(chalk.bold('Support:'), reg.model ? (supported ? chalk.green('At least basic') : chalk.yellow('Generic')) : chalk.yellow('Unknown'));
log();
Expand Down
28 changes: 28 additions & 0 deletions lib/connectToDevice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

const createDevice = require('./createDevice');

module.exports = function(options) {
let device = createDevice(options);
return device.call('miIO.info')
.then(data => {
if(options.model) {
// If the model was specified we reuse the device instance
} else {
// If the model was automatically discovered recreate the device
device.destroy();
device = createDevice(Object.assign({}, options, {
model: data.model,
token: data.token
}));
}
})
.then(() => {
return device.init()
.then(() => device);
})
.catch(err => {
device.destroy();
throw err;
});
};
18 changes: 18 additions & 0 deletions lib/createDevice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

const Device = require('./device');
const models = require('./models');

module.exports = function(options) {
if(! options.address) throw new Error('Address to device is required');

const d = models[options.model];
let device;
if(! d) {
device = new Device(options);
} else {
device = new d(options);
}

return device;
};
127 changes: 125 additions & 2 deletions lib/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const infoFromHostname = require('./infoFromHostname');
const Packet = require('./packet');
const Tokens = require('./tokens');

const connectToDevice = require('./connectToDevice');

const PORT = 54321;

class Browser {
Expand All @@ -30,8 +32,8 @@ class Browser {
this._events.on(event, cb);
}

off(event, cb) {
this._events.off(event, cb);
removeListener(event, cb) {
this._events.removeListener(event, cb);
}

start() {
Expand Down Expand Up @@ -162,4 +164,125 @@ class Browser {
}
}

class Devices {
constructor(options) {
this._events = new EventEmitter();

this._filter = options && options.filter;
this._skipSubDevices = options && options.skipSubDevices;
this._devices = {};

this._browser = new Browser(options);
this._browser.on('available', this._serviceAvailable.bind(this));
this._browser.on('unavailable', this._serviceUnavailable.bind(this));
}

on(event, cb) {
this._events.on(event, cb);
}

removeListener(event, cb) {
this._events.removeListener(event, cb);
}

start() {
this._browser.start();
}

stop() {
this._browser.stop();
}

_serviceAvailable(service) {
if(this._filter && ! this._filter(service)) {
// Filter does not match this device
return;
}

let reg = this._devices[service.id];
if(! reg) {
reg = this._devices[service.id] = Object.assign({
device: null
}, service);
}

// Return if we are already connecting to this device
if(reg.connectionPromise) return;

if(reg.token) {
// This device has a token so it's possible to connect to it
reg.connectionPromise = connectToDevice(service)
.then(device => {
reg.device = device;
this._events.emit('available', reg);

if(device.type === 'gateway') {
this._bindSubDevices(device);
}
})
.catch(err => {
reg.error = err;
this._events.emit('available', reg);

err.device = service;
this._events.emit('error', err);
})
.then(() => {
delete reg.connectionPromise;
});
} else {
// There is no token so emit even directly
this._events.emit('available', reg);
}
}

_serviceUnavailable(service) {
const reg = this._devices[service.id];
if(! reg) return;

if(reg.device) {
reg.device.destroy();
}
delete this._devices[service.id];
this._events.emit('unavailable', reg);

Object.keys(this._devices).forEach(key => {
const subReg = this._devices[key];
if(subReg.parent && subReg.parent.id == service.id) {
// This device belongs to the service being removed
delete this._devices[key];
subReg.device.destroy();
this._events.emit('unavailable', subReg);
}
});
}

_bindSubDevices(device) {
if(this._skipSubDevices) return;

device.on('deviceAvailable', sub => {
const reg = {
id: sub.id,
model: sub.model,
type: sub.type,

parent: device,
device: sub
};

if(this._filter && ! this._filter(reg)) {
// Filter does not match sub device
return;
}

// Register and emit event
this._devices[sub.id] = reg;
this._events.emit('available', reg);
});

device.on('deviceUnavailable', sub => this._serviceUnavailable(sub));
}
}

module.exports.Browser = Browser;
module.exports.Devices = Devices;
56 changes: 16 additions & 40 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const discovery = require('./discovery');
const Device = require('./device');

module.exports.Device = Device;
const models = module.exports.models = require('./models');
module.exports.models = require('./models');

/**
* Resolve a device from the given options.
Expand All @@ -14,31 +14,7 @@ const models = module.exports.models = require('./models');
* * `port`, optional port number, if not specified the default 54321 will be used
* * `token`, optional token of the device
*/
module.exports.device = function(options) {
if(! options.address) throw new Error('Address to device is required');

// Create a temporary device and fetch info from device
const temp = new Device(options);
return temp.call('miIO.info')
.then(data => {
temp.destroy();
return createDevice({
id: options.id,
address: options.address,
port: options.port,
model: data.model,
token: data.token
});
})
.then(device => {
return device.init()
.then(() => device);
})
.catch(err => {
temp.destroy();
throw err;
});
};
module.exports.device = require('./connectToDevice');

/**
* Create a device from the given options. This will either create a
Expand All @@ -50,25 +26,25 @@ module.exports.device = function(options) {
* * `model`, optional model if known, allows a more specific type to be returned
* * `token`, optional token of the device
*/
const createDevice = module.exports.createDevice = function(options) {
if(! options.address) throw new Error('Address to device is required');

const d = models[options.model];
let device;
if(! d) {
device = new Device(options);
} else {
device = new d(options);
}

return device;
};
module.exports.createDevice = require('./createDevice');

/**
* Extract information about a device from its hostname on the local network.
*/
module.exports.infoFromHostname = require('./infoFromHostname.js');
module.exports.infoFromHostname = require('./infoFromHostname');

/**
* Browse for devices available on the network. Will not automatically
* connect to them.
*/
module.exports.browse = function(options) {
return new discovery.Browser(options || {});
};

/**
* Get access to all devices on the current network. Will find and connect to
* devices automatically.
*/
module.exports.devices = function(options) {
return new discovery.Devices(options || {});
};

0 comments on commit d7789b5

Please sign in to comment.