hueadm
Version:
A command line management interface to phillips hue
354 lines (308 loc) • 9.03 kB
JavaScript
var fs = require('fs');
var util = require('util');
var autocast = require('autocast');
var csscolors = require('css-color-names');
var deepmerge = require('deepmerge');
var mired = require('mired');
var rgb2hsl = require('color-convert').rgb.hsl;
var yaml = require('yamljs');
var verror = require('verror');
module.exports.getCliTableOptions = getCliTableOptions;
module.exports.kvToObj = kvToObj;
module.exports.filterArrayByKv = filterArrayByKv;
module.exports.human = human;
module.exports.hex2rgb = hex2rgb;
module.exports.createLightState = createLightState;
module.exports.makeFinisher = makeFinisher;
/*
* take some basic information and return node-cmdln options suitable for
* tabula
*
* @param {String} (optional) opts.columnDefault Default value for `-o`
* @param {String} (optional) opts.sortDefault Default value for `-s`
* @param {String} (optional) opts.includeLong Include `-l` option
* @return {Array} Array of cmdln options objects
*
* taken from https://github.com/joyent/node-triton/blob/master/lib/common.js
*/
function getCliTableOptions(opts) {
opts = opts || {};
var o;
// construct the options object
var tOpts = [];
// header
tOpts.push({
group: 'Output options'
});
// -H
tOpts.push({
names: ['H'],
type: 'bool',
help: 'Omit table header row.'
});
// -o field1,field2,...
o = {
names: ['o'],
type: 'string',
help: 'Specify fields (columns) to output.',
helpArg: 'field1,...'
};
if (opts.columnsDefault) {
o.default = opts.columnsDefault;
}
tOpts.push(o);
// -l, --long
if (opts.includeLong) {
tOpts.push({
names: ['long', 'l'],
type: 'bool',
help: 'Long/wider output. Ignored if "-o ..." is used.'
});
}
// -s field1,field2,...
o = {
names: ['s'],
type: 'string',
help: 'Sort on the given fields.',
helpArg: 'field1,...'
};
if (opts.sortDefault) {
o.default = opts.sortDefault;
o.help = util.format('%s Default is "%s".', o.help, opts.sortDefault);
}
tOpts.push(o);
// -j, --json
tOpts.push({
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
});
return tOpts;
}
/**
* given an array of key=value pairs, break them into an object
*
* @param {Array} kvs - an array of key=value pairs
*
* taken from https://github.com/joyent/node-triton/blob/master/lib/common.js
*/
function kvToObj(kvs, opts) {
opts = opts || {};
// short-circuit here if stdin is allowed and `-` is passed as the first
// argument
if (opts.allowStdin && kvs[0] === '-') {
return JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
}
var o = {};
for (var i = 0; i < kvs.length; i++) {
var kv = kvs[i];
var idx = kv.indexOf('=');
if (idx === -1) {
throw new Error(util.format(
'invalid filter: "%s" (must be of the form "field=value")',
kv));
}
var k = kv.slice(0, idx);
var v = kv.slice(idx + 1);
if (opts.autocast) {
v = autocast(v);
}
var keys = k.split('.');
var key;
var _o = o;
for (var j = 0; j < keys.length - 1; j++) {
key = keys[j];
_o[key] = _o[key] || {};
_o = _o[key];
}
key = keys[keys.length - 1];
if (_o[key] === undefined) {
_o[key] = v;
} else if (Array.isArray(_o[key])) {
_o[key].push(v);
} else {
_o[key] = [_o[key], v];
}
}
return o;
}
/**
* filter an array of objects by passing in a key/value set created by
* kvToObj
*/
function filterArrayByKv(arr, kvs) {
// filter based on listOpts
var filters = Object.keys(kvs);
return arr.filter(function (o) {
for (var i = 0; i < filters.length; i++) {
var key = filters[i];
if (o[key] !== kvs[key]) {
return false;
}
}
return true;
});
}
/**
* human time
*/
function human(seconds) {
if (seconds instanceof Date) {
seconds = Math.round((Date.now() - seconds) / 1000);
}
seconds = Math.abs(seconds);
var times = [
seconds / 60 / 60 / 24 / 365, // years
seconds / 60 / 60 / 24 / 7, // weeks
seconds / 60 / 60 / 24, // days
seconds / 60 / 60, // hours
seconds / 60, // minutes
seconds // seconds
];
var names = ['y', 'w', 'd', 'h', 'm', 's'];
for (var i = 0; i < names.length; i++) {
var time = Math.floor(times[i]);
if (time > 0) {
return util.format('%d%s', time, names[i]);
}
}
return '0s';
}
// convert a 3 or 6 character hex string to rgb
function hex2rgb(hex) {
var r, g, b;
if (hex[0] === '#') {
hex = hex.slice(1);
}
switch (hex.length) {
case 3:
r = todec(hex[0], hex[0]);
g = todec(hex[1], hex[1]);
b = todec(hex[2], hex[2]);
break;
case 6:
r = todec(hex[0], hex[1]);
g = todec(hex[2], hex[3]);
b = todec(hex[4], hex[5]);
break;
default:
throw new Error('invalid hex');
}
return [r, g, b];
function todec(h, i) {
return parseInt(h + '' + i, 16);
}
}
// create a light state object with opts
function createLightState(args) {
// create a light state object
var state = {
on: true
};
var command = args.shift();
var match;
if (command === 'on') {
state.on = true;
} else if (command === 'off') {
state.on = false;
} else if (command === 'clear') {
state.alert = 'none';
state.effect = 'none';
} else if (command === 'reset') {
state.alert = 'none';
state.effect = 'none';
state.ct = Math.round(mired.kelvinToMired(2700));
state.bri = 254;
} else if (command === 'select') {
state.alert = 'select';
} else if (command === 'lselect') {
state.alert = 'lselect';
} else if (command === 'colorloop') {
state.effect = 'colorloop';
} else if (command === '-') {
state = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
} else if ((match = command.match(/^([-+=])([0-9]+)(%?)$/))) {
var op = match[1];
var num = parseInt(match[2], 10);
var perc = match[3];
if (perc) {
num = Math.round(num * (254 / 100));
}
switch (op) {
case '=':
state.bri = num;
break;
case '-':
state.bri_inc = -num;
break;
case '+':
state.bri_inc = num;
break;
}
} else if ((match = command.match(/^([0-9]+)[kK]$/))) {
var kelvin = +match[1];
var ct = mired.kelvinToMired(kelvin);
state.ct = Math.round(ct);
}
else if ((match = command.match(/^\bscene=\b(.*)$/))) {
var sceneId = match[1];
state = { "scene": sceneId };
} else {
var hex = csscolors[command];
if (!hex) {
match = command.match(/^#?(([0-9A-Fa-f]{3})|([0-9A-Fa-f]{6}))$/);
if (!match) {
return new Error('failed to parse command');
}
hex = match[1];
}
var rgb = hex2rgb(hex);
var hsl = rgb2hsl(rgb);
state.hue = Math.round(hsl[0] / 360 * 65535);
state.sat = Math.round(hsl[1] / 100 * 254);
state.bri = Math.round(hsl[2] / 100 * 254);
}
// parse extra options to merge into the state object
var extraOpts;
try {
extraOpts = kvToObj(args, {autocast: true});
} catch (e) {
return e;
}
return deepmerge(state, extraOpts);
}
/*
* Create a callback function fit for use by most commands
* that prints output in either yaml or json format
*/
function makeFinisher(opts, cb) {
function finish(err, result) {
if (err) {
cb(err);
return;
}
var errors = [];
var s;
if (opts.json) {
s = JSON.stringify(result, null, 2);
} else {
s = yaml.stringify(result);
}
// if the result is an array of objects, check for any errors
if (Array.isArray(result)) {
result.forEach(function (res) {
if (typeof(res) !== 'object')
return;
if (!res.hasOwnProperty('error'))
return;
var error = new Error(res.error.description);
error.type = res.error.type;
error.address = res.error.address;
errors.push(error);
});
}
console.log(s);
cb(verror.errorFromList(errors));
}
return finish;
}