diff --git a/README.md b/README.md index 877d513..9ce2790 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Use Huejay to interact with Philips Hue in the following ways: * Manage portal settings * Manage software updates * Manage users +* Manage lights ## Installation diff --git a/examples/get-lights.js b/examples/get-lights.js new file mode 100755 index 0000000..730723c --- /dev/null +++ b/examples/get-lights.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +'use strict'; + +let huejay = require('../lib/Huejay'); +let credentials = require('./.credentials.json'); + +let client = new huejay.Client(credentials); + +console.log('Retrieving lights...'); +console.log(); + +client.getLights() + .then(lights => { + for (let light of lights) { + console.log(`Light [${light.id}]: ${light.name}`); + console.log(` Type: ${light.type}`); + console.log(` Unique ID: ${light.uniqueId}`); + console.log(` Manufacturer: ${light.manufacturer}`); + console.log(` Model Id: ${light.modelId}`); + console.log(` Software Version: ${light.softwareVersion}`); + console.log(' State:'); + console.log(` On: ${light.on}`); + console.log(` Reachable: ${light.reachable}`); + console.log(` Brightness: ${light.brightness}`); + console.log(` Color mode: ${light.colorMode}`); + console.log(` Hue: ${light.hue}`); + console.log(` Saturation: ${light.saturation}`); + console.log(` X/Y: ${light.xy[0]}, ${light.xy[1]}`); + console.log(` Color Temp: ${light.colorTemp}`); + console.log(` Alert: ${light.alert}`); + console.log(` Effect: ${light.effect}`); + console.log(); + } + }) + .catch(error => { + console.log(error.stack); + }); diff --git a/examples/get-new-lights.js b/examples/get-new-lights.js new file mode 100755 index 0000000..f14ce37 --- /dev/null +++ b/examples/get-new-lights.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +'use strict'; + +let huejay = require('../lib/Huejay'); +let credentials = require('./.credentials.json'); + +let client = new huejay.Client(credentials); + +console.log('Retrieving new lights...'); +console.log(); + +client.getNewLights() + .then(lights => { + for (let light of lights) { + console.log(light); + console.log(); + } + }) + .catch(error => { + console.log(error.stack); + }); diff --git a/examples/save-light.js b/examples/save-light.js new file mode 100755 index 0000000..6e6fe75 --- /dev/null +++ b/examples/save-light.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +'use strict'; + +let huejay = require('../lib/Huejay'); +let credentials = require('./.credentials.json'); + +let client = new huejay.Client(credentials); + +console.log(`Retrieving light from (${credentials.host})...`); + +client.getLights() + .then(lights => { + let light = lights[4]; + + light.name = `Name test`; + light.on = true; + + console.log(`Saving light...`); + + return client.saveLight(light); + }) + .then(() => { + console.log('Success'); + }) + .catch(error => { + console.log(error.stack); + }); diff --git a/examples/start-light-scan.js b/examples/start-light-scan.js new file mode 100755 index 0000000..52d6868 --- /dev/null +++ b/examples/start-light-scan.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +'use strict'; + +let huejay = require('../lib/Huejay'); +let credentials = require('./.credentials.json'); + +let client = new huejay.Client(credentials); + +console.log('Starting light scan...'); + +client.startLightScan() + .then(() => { + console.log('Success'); + }) + .catch(error => { + console.log(error.stack); + }); diff --git a/lib/Bridge.js b/lib/Bridge.js index 1c6db3f..e629351 100644 --- a/lib/Bridge.js +++ b/lib/Bridge.js @@ -38,23 +38,7 @@ class Bridge { */ constructor(attributes) { this.setAttributes(attributes); - this.resetChangedValues(); - } - - /** - * Set attributes - * - * @param {Object} attributes Attributes - */ - setAttributes(attributes) { - this.attributes = {}; - - for (let key in attributes) { - let attributeKey = key; - if (key in ATTRIBUTE_MAP) { - this.attributes[ATTRIBUTE_MAP[key]] = attributes[key]; - } - } + this.resetChangedAttributes(); } /** @@ -83,7 +67,7 @@ class Bridge { set name(name) { this.attributes.name = name; - this.changed.push('name'); + this.changedAttributes.push('name'); } /** @@ -148,7 +132,7 @@ class Bridge { set zigbeeChannel(channel) { this.attributes.zigbeeChannel = Number(channel); - this.changed.push('zigbeeChannel'); + this.changedAttributes.push('zigbeeChannel'); } /** @@ -177,7 +161,7 @@ class Bridge { set ipAddress(ipAddress) { this.attributes.ipAddress = String(ipAddress); - this.changed.push('ipAddress'); + this.changedAttributes.push('ipAddress'); } /** @@ -197,7 +181,7 @@ class Bridge { set dhcpEnabled(value) { this.attributes.dhcp = Boolean(value); - this.changed.push('dhcp'); + this.changedAttributes.push('dhcp'); } /** @@ -217,7 +201,7 @@ class Bridge { set netmask(mask) { this.attributes.netmask = String(mask); - this.changed.push('netmask'); + this.changedAttributes.push('netmask'); } @@ -238,7 +222,7 @@ class Bridge { set gateway(address) { this.attributes.gateway = String(address); - this.changed.push('gateway'); + this.changedAttributes.push('gateway'); } /** @@ -262,7 +246,7 @@ class Bridge { set proxyAddress(address) { this.attributes.proxyAddress = String(address); - this.changed.push('proxyAddress'); + this.changedAttributes.push('proxyAddress'); } /** @@ -284,7 +268,7 @@ class Bridge { set proxyPort(port) { this.attributes.proxyPort = Number(port); - this.changed.push('proxyPort'); + this.changedAttributes.push('proxyPort'); } /** @@ -317,7 +301,7 @@ class Bridge { set timeZone(timeZone) { this.attributes.timeZone = String(timeZone); - this.changed.push('timeZone'); + this.changedAttributes.push('timeZone'); } /** @@ -368,7 +352,7 @@ class Bridge { set linkButtonEnabled(value) { this.attributes.linkButton = Boolean(value); - this.changed.push('linkButton'); + this.changedAttributes.push('linkButton'); } /** @@ -388,29 +372,45 @@ class Bridge { set touchLinkEnabled(value) { this.attributes.touchLink = Boolean(value); - this.changed.push('touchLink'); + this.changedAttributes.push('touchLink'); + } + + /** + * Set attributes + * + * @param {Object} attributes Attributes + */ + setAttributes(attributes) { + this.attributes = {}; + + for (let key in attributes) { + let attributeKey = key; + if (key in ATTRIBUTE_MAP) { + this.attributes[ATTRIBUTE_MAP[key]] = attributes[key]; + } + } } /** - * Get changed + * Get changed attributes * - * @return {array} List of changed values (with original keys) + * @return {array} List of changed attributes */ - getChangedValues() { - let changedValues = {}; + getChangedAttributes() { + let changedAttributes = {}; - for (let key of this.changed) { - changedValues[key] = this.attributes[key]; + for (let key of this.changedAttributes) { + changedAttributes[key] = this.attributes[key]; } - return changedValues; + return changedAttributes; } /** - * Reset changed values + * Reset changed attributes */ - resetChangedValues() { - this.changed = []; + resetChangedAttributes() { + this.changedAttributes = []; } /** diff --git a/lib/Client.js b/lib/Client.js index ed66223..b288688 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -215,6 +215,69 @@ class Client { return this.invokeCommand(new DeleteUser(username)); } + /** + * Start light scan + * + * @return {Promise} Promise for chaining + */ + startLightScan() { + let StartLightScan = require('./Command/StartLightScan'); + + return this.invokeCommand(new StartLightScan); + } + + /** + * Get new lights + * + * @return {Promise} Promise for chaining + */ + getNewLights() { + let GetNewLights = require('./Command/GetNewLights'); + + return this.invokeCommand(new GetNewLights); + } + + /** + * Get lights + * + * @return {Promise} Promise for chaining + */ + getLights() { + let GetLights = require('./Command/GetLights'); + + return this.invokeCommand(new GetLights); + } + + /** + * Save light + * + * @param {Light} light Light + * + * @return {Promise} Promise for chaining + */ + saveLight(light) { + let SaveLight = require('./Command/SaveLight'); + let SaveLightState = require('./Command/SaveLightState'); + + return Promise.all([ + this.invokeCommand(new SaveLight(light)), + this.invokeCommand(new SaveLightState(light)) + ]); + } + + /** + * Delete light + * + * @param {mixed} lightId Light Id or Light object + * + * @return {Promise} Promise for chaining + */ + deleteLight(lightId) { + let DeleteLight = require('./Command/DeleteLight'); + + return this.invokeCommand(new DeleteLight(light)); + } + /** * Get time zones * diff --git a/lib/Command/DeleteLight.js b/lib/Command/DeleteLight.js new file mode 100644 index 0000000..ac41a80 --- /dev/null +++ b/lib/Command/DeleteLight.js @@ -0,0 +1,37 @@ +'use strict'; + +/** + * Delete light command + * + * Delete a light by id + */ +class DeleteLight { + /** + * Constructor + * + * @param {string} lightId Light Id or Light object + */ + constructor(lightId) { + this.lightId = String(lightId); + } + + /** + * Invoke command + * + * @param {Client} client Client + * + * @return {Promise} Promise for chaining + */ + invoke(client) { + let options = { + method: 'DELETE', + path: `api/${client.username}/lights/${this.lightId}` + }; + + return client.getTransport() + .sendRequest(options) + .then(() => true); + } +} + +module.exports = DeleteLight; diff --git a/lib/Command/GetLights.js b/lib/Command/GetLights.js new file mode 100644 index 0000000..9595102 --- /dev/null +++ b/lib/Command/GetLights.js @@ -0,0 +1,33 @@ +'use strict'; + +let Light = require('../Light'); + +/** + * Get lights command + * + * Get a list of lights + */ +class GetLights { + /** + * Invoke command + * + * @param {Client} client Client + * + * @return {Promise} Promise for chaining + */ + invoke(client) { + let options = { + path: `api/${client.username}/lights` + }; + + return client.getTransport() + .sendRequest(options) + .then(result => { + return Object.keys(result).map(lightId => { + return new Light(lightId, result[lightId], result[lightId].state); + }) + }); + } +} + +module.exports = GetLights; diff --git a/lib/Command/GetNewLights.js b/lib/Command/GetNewLights.js new file mode 100644 index 0000000..6e4129b --- /dev/null +++ b/lib/Command/GetNewLights.js @@ -0,0 +1,46 @@ +'use strict'; + +let GetLights = require('./GetLights'); + +/** + * Get new lights command + * + * Get a list of new lights + */ +class GetNewLights { + /** + * Invoke command + * + * @param {Client} client Client + * + * @return {Promise} Promise for chaining + */ + invoke(client) { + let options = { + path: `api/${client.username}/lights/new` + }; + + return client.getTransport() + .sendRequest(options) + .then(result => { + delete result.lastscan; + + let newLightIds = Object.keys(result); + + return (new GetLights).invoke(client) + .then(lights => { + let newLights = []; + + for (let light of lights) { + if (newLightIds.indexOf(light.id) > -1) { + newLights.push(light); + } + } + + return newLights; + }); + }); + } +} + +module.exports = GetNewLights; diff --git a/lib/Command/SaveBridge.js b/lib/Command/SaveBridge.js index 1f07731..a33043a 100644 --- a/lib/Command/SaveBridge.js +++ b/lib/Command/SaveBridge.js @@ -31,8 +31,8 @@ class SaveBridge { constructor(bridge) { validateBridge(bridge); - this.bridge = bridge; - this.changedValues = bridge.getChangedValues(); + this.bridge = bridge; + this.changedAttributes = bridge.getChangedAttributes(); } /** @@ -43,31 +43,29 @@ class SaveBridge { * @return {Promise} Promise for chaining */ invoke(client) { - let options = { - method: 'PUT', - path: `api/${client.username}/config`, - body: {} - }; - let promises = []; - for (let key in this.changedValues) { + for (let key in this.changedAttributes) { if (key in ATTRIBUTE_MAP) { promises.push( - this.saveBridgeSetting(client, ATTRIBUTE_MAP[key], this.changedValues[key]) + this.saveBridgeAttribute( + client, + ATTRIBUTE_MAP[key], + this.changedAttributes[key] + ) ); } } return Promise.all(promises) .then(results => { - this.bridge.resetChangedValues(); + this.bridge.resetChangedAttributes(); return true; }); } /** - * Save bridge setting + * Save bridge attribute * * @param {Client} client Client * @param {string} attribute Attribute @@ -75,7 +73,7 @@ class SaveBridge { * * @return {Promise} Promise for chaining */ - saveBridgeSetting(client, attribute, value) { + saveBridgeAttribute(client, attribute, value) { let options = { method: 'PUT', path: `api/${client.username}/config`, diff --git a/lib/Command/SaveLight.js b/lib/Command/SaveLight.js new file mode 100644 index 0000000..2addd9c --- /dev/null +++ b/lib/Command/SaveLight.js @@ -0,0 +1,79 @@ +'use strict'; + +let Light = require('../Light'); + +const ATTRIBUTE_MAP = { + 'name': 'name', +}; + +/** + * Save light command + * + * Saves light + */ +class SaveLight { + /** + * Constructor + * + * @param {Light} light Light + */ + constructor(light) { + Light.validateLight(light); + + this.light = light; + this.changedAttributes = light.getChangedAttributes(); + } + + /** + * Invoke command + * + * @param {Client} client Client + * + * @return {Promise} Promise for chaining + */ + invoke(client) { + let promises = []; + for (let key in this.changedAttributes) { + if (key in ATTRIBUTE_MAP) { + promises.push( + this.saveLightAttribute( + client, + ATTRIBUTE_MAP[key], + this.changedAttributes[key] + ) + ); + } + } + + return Promise.all(promises) + .then(results => { + this.light.resetChangedAttributes(); + + return true; + }); + } + + /** + * Save light attribute + * + * @param {Client} client Client + * @param {string} attribute Attribute + * @param {mixed} value Value + * + * @return {Promise} Promise for chaining + */ + saveLightAttribute(client, attribute, value) { + let options = { + method: 'PUT', + path: `api/${client.username}/lights/${this.light.id}`, + body: {} + }; + + options.body[attribute] = value; + + return client.getTransport() + .sendRequest(options); + } +} + +module.exports = SaveLight; diff --git a/lib/Command/SaveLightState.js b/lib/Command/SaveLightState.js new file mode 100644 index 0000000..770a84a --- /dev/null +++ b/lib/Command/SaveLightState.js @@ -0,0 +1,61 @@ +'use strict'; + +let Light = require('../Light'); + +const STATE_MAP = { + 'on': 'on', + 'brightness': 'bri', + 'hue': 'hue', + 'saturation': 'sat', + 'xy': 'xy', + 'colorTemp': 'ct', + 'transitionTime': 'transitiontime', + 'alert': 'alert', + 'effect': 'effect', +}; + +/** + * Save light state command + * + * Saves light state + */ +class SaveLightState { + /** + * Constructor + * + * @param {Light} light Light + */ + constructor(light) { + Light.validateLight(light); + + this.light = light; + this.changedState = light.getChangedState(); + } + + /** + * Invoke command + * + * @param {Client} client Client + * + * @return {Promise} Promise for chaining + */ + invoke(client) { + let options = { + method: 'PUT', + path: `api/${client.username}/lights/${this.light.id}/state`, + body: {} + }; + + for (let key in this.changedState) { + if (key in STATE_MAP) { + options.body[STATE_MAP[key]] = this.changedState[key]; + } + } + + return client.getTransport() + .sendRequest(options) + .then(() => true); + } +} + +module.exports = SaveLightState; diff --git a/lib/Command/StartLightScan.js b/lib/Command/StartLightScan.js new file mode 100644 index 0000000..0d0c76d --- /dev/null +++ b/lib/Command/StartLightScan.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * Start light scan command + * + * Start a scan for new lights + */ +class StartLightScan { + /** + * Invoke command + * + * @param {Client} client Client + * + * @return {Promise} Promise for chaining + */ + invoke(client) { + let options = { + method: 'POST', + path: `api/${client.username}/lights` + }; + + return client.getTransport() + .sendRequest(options) + .then(() => true); + } +} + +module.exports = StartLightScan; diff --git a/lib/Light.js b/lib/Light.js new file mode 100644 index 0000000..efd63c8 --- /dev/null +++ b/lib/Light.js @@ -0,0 +1,431 @@ +'use strict'; + +let Error = require('./Error'); + +const ATTRIBUTE_MAP = { + 'id': 'id', + 'name': 'name', + 'type': 'type', + 'uniqueid': 'uniqueId', + 'manufacturername': 'manufacturer', + 'modelid': 'modelId', + 'swversion': 'softwareVersion', +}; + +const STATE_MAP = { + 'on': 'on', + 'reachable': 'reachable', + 'bri': 'brightness', + 'colormode': 'colorMode', + 'hue': 'hue', + 'sat': 'saturation', + 'xy': 'xy', + 'ct': 'colorTemp', + 'transitiontime': 'transitionTime', + 'alert': 'alert', + 'effect': 'effect', +}; + +/** + * Light + * + * Light object + */ +class Light { + /** + * Constructor + * + * @param {Object} attributes Attributes + * @param {Object} state State + */ + constructor(id, attributes, state) { + attributes.id = id; + + this.setAttributes(attributes); + this.resetChangedAttributes(); + + this.setState(state); + this.resetChangedState(); + } + + /** + * Validate light + * + * @param {Light} light Light + */ + static validateLight(light) { + validateLight(light); + } + + /** + * Get light id + * + * @return {string} Light id + */ + get id() { + return this.attributes.id; + } + + /** + * Get name + * + * @return {string} Name + */ + get name() { + return this.attributes.name; + } + + /** + * Set name + * + * @param {string} name Name + */ + set name(name) { + this.attributes.name = String(name); + + this.changedAttributes.push('name'); + } + + /** + * Get type + * + * @return {string} Type + */ + get type() { + return this.attributes.type; + } + + /** + * Get unique id + * + * @return {string} Unique id + */ + get uniqueId() { + return this.attributes.uniqueId; + } + + /** + * Get manufacturer + * + * @return {string} Manufacturer + */ + get manufacturer() { + return this.attributes.manufacturer; + } + + /** + * Get model id + * + * @return {string} Model id + */ + get modelId() { + return this.attributes.modelId; + } + + /** + * Get software version + * + * @return {string} Software version + */ + get softwareVersion() { + return this.attributes.softwareVersion; + } + + /** + * Get on + * + * @return {bool} True if on, false if not + */ + get on() { + return Boolean(this.state.on); + } + + /** + * Set on + * + * @param {bool} value True for on, false if not + */ + set on(value) { + this.state.on = Boolean(value); + + this.changedState.push('on'); + } + + /** + * Get reachable + * + * @return {bool} Reachable + */ + get reachable() { + return this.state.reachable; + } + + /** + * Get brightness + * + * @return {int} Brightness + */ + get brightness() { + return this.state.brightness; + } + + /** + * Set brightness + * + * @param {int} brightness Brightness + */ + set brightness(brightness) { + this.state.brightness = Number(brightness); + + this.changedState.push('brightness'); + } + + /** + * Get color mode + * + * @return {string} Color mode + */ + get colorMode() { + return this.state.colorMode; + } + + /** + * Get hue + * + * @return {int} Hue + */ + get hue() { + return this.state.hue; + } + + /** + * Set hue + * + * @param {int} hue Hue + */ + set hue(hue) { + this.state.hue = Number(hue); + + this.changedState.push('hue'); + } + + /** + * Get saturation + * + * @return {int} Saturation + */ + get saturation() { + return this.state.saturation; + } + + /** + * Set saturation + * + * @param {int} saturation Saturation + */ + set saturation(saturation) { + this.state.saturation = Number(saturation); + + this.changedState.push('saturation'); + } + + /** + * Get x/y + * + * @return {array} X/Y + */ + get xy() { + return this.state.xy; + } + + /** + * Set x/y + * + * @param {array} xy X/Y + */ + set xy(xy) { + this.state.xy = xy; + + this.changedState.push('xy'); + } + + /** + * Get color temperature + * + * @return {int} Temperature + */ + get colorTemp() { + return this.state.colorTemp; + } + + /** + * Set color temperature + * + * @param {int} temp Temperature + */ + set colorTemp(temp) { + this.state.colorTemp = Number(temp); + + this.changedState.push('colorTemp'); + } + + /** + * Get transition time + * + * @return {int} Transition time + */ + get transitionTime() { + return this.state.transitionTime / 10; + } + + /** + * Set transition time + * + * @param {int} time Time in seconds + */ + set transitionTime(time) { + this.state.transitionTime = Number(time) * 10; + + this.changedState.push('transitionTime'); + } + + /** + * Get alert + * + * @return {string} Alert + */ + get alert() { + return this.state.alert; + } + + /** + * Set alert + * + * @param {string} mode Mode + */ + set alert(mode) { + this.state.alert = String(mode); + + this.changedState.push('alert'); + } + + /** + * Get effect + * + * @return {string} Effect + */ + get effect() { + return this.state.effect; + } + + /** + * Set effect + * + * @param {string} effect Effect + */ + set effect(effect) { + this.state.effect = String(effect); + + this.changedState.push('effect'); + } + + /** + * Set attributes + * + * @param {Object} attributes Attributes + */ + setAttributes(attributes) { + this.attributes = {}; + + for (let key in attributes) { + if (key in ATTRIBUTE_MAP) { + this.attributes[ATTRIBUTE_MAP[key]] = attributes[key]; + } + } + } + + /** + * Get changed attributes + * + * @return {array} List of changed attributes + */ + getChangedAttributes() { + let changedAttributes = {}; + + for (let key of this.changedAttributes) { + changedAttributes[key] = this.attributes[key]; + } + + return changedAttributes; + } + + /** + * Reset changed values + */ + resetChangedAttributes() { + this.changedAttributes = []; + } + + /** + * Set state + * + * @param {Object} state State + */ + setState(state) { + this.state = {}; + + for (let key in state) { + if (key in STATE_MAP) { + this.state[STATE_MAP[key]] = state[key]; + } + } + } + + /** + * Get changed state + * + * @return {array} List of changed state + */ + getChangedState() { + let changedState = {}; + + for (let key of this.changedState) { + changedState[key] = this.state[key]; + } + + return changedState; + } + + /** + * Reset changed state + */ + resetChangedState() { + this.changedState = []; + } + + /** + * To string + * + * @return {string} Id + */ + toString() { + return this.id; + } +} + +/** + * Validate light + * + * @param {mixed} light Light object + * + * @return {bool} True if valid + */ +function validateLight(light) { + if (!(light instanceof Light)) { + throw Error({ + description: 'Expecting Light' + }); + } +} + +module.exports = Light; diff --git a/package.json b/package.json index 4d09939..3ecdef8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "huejay", - "version": "0.5.1", + "version": "0.6.0", "description": "Philips Hue client for Node.js", "keywords": [ "hue",