diff --git a/.gitignore b/.gitignore index 178c3cf..851c424 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ jspm_packages .yarn-integrity .idea - +.history +.vscode diff --git a/README.md b/README.md index 8b48a0f..f228586 100644 --- a/README.md +++ b/README.md @@ -2,34 +2,52 @@ This Node-RED node is for controlling tp-link Wi-Fi Smart Plug - Model HS100. -This node has only been tested with a HS100(UK). The HS100 is also available in US and EU plug versions. We expect they will work too. +This node has only been tested with a HS100(UK). The HS100 is also available in US and EU plug +versions. We expect they will work too. -This node simply wraps the excellent work here https://github.com/czjs2/hs100-api. +This node simply wraps the excellent work here https://github.com/czjs2/hs100-api. # Installation Change directory to your node red installation: $ npm install node-red-contrib-hs100 - + Alternatively, use the Palette Manager in Node-RED. # Configuration -Drag this node on to a worksheet and double click it. Enter the IP address of the plug on your network. Save and deploy. - - -# Actuation +Drag this node on to a worksheet and double click it. Enter the IP address of the plug on your +network. Save and deploy. + +# Actuations + +This node supports a number of actuations that are invoked by sending a msg.topic or msg.payload +to the node's input. + +| Topic/Payload | Description | +| ------------------ | :----------------------------------------------------------------------------------: | +| on | To turn the HS100 on. | +| off | To turn the HS100 off. | +| info | Get all plug info, combination of sysinfo, cloudinfo consumption, schedulenextaction | +| sysinfo | Get general plug information. | +| cloudinfo | Get TP-Link Cloud information. | +| consumption | Get power consumption data | +| powerstate | Returns true if plug is on. | +| schedulenextaction | | +| schedulerules | | +| awayrules | | +| timerrules | | +| time | | +| timezone | | +| scaninfo | | +| model | | ## Turn on -To turn the HS100 on, send a message to this node's input with the topic or payload set to `on`. - ## Turn off -To turn the HS100 off, send a message to this node's input with the topic or payload set to `off`. - ## Obtain power consumption data -To obtain the power consumption, send a message to this node's input with the topic or payload set to `consumption`. -The consumption data will be sent via this node's output in `msg.payload`. +To obtain the power consumption, send a message to this node's input with the topic or payload +set to `consumption`. The consumption data will be sent via this node's output in `msg.payload`. diff --git a/index.js b/index.js index 8f4855e..aff59d4 100644 --- a/index.js +++ b/index.js @@ -22,34 +22,64 @@ THE SOFTWARE. */ -module.exports = function (RED) { +module.exports = function hs100(RED) { 'use strict'; var Hs100Api = require('fx-hs100-api'); - RED.nodes.registerType('hs100', function (config) { + // TODO address the disparity of not having on and off in here. It bothers me. + hs100.supportedActuations = { + info: 'getInfo', + sysinfo: 'getSysInfo', + cloudinfo: 'getCloudInfo', + consumption: 'getConsumption', + powerstate: 'getPowerState', + schedulenextaction: 'getScheduleNextAction', + schedulerules: 'getScheduleRules', + awayrules: 'getAwayRules', + timerrules: 'getTimerRules', + time: 'getTime', + timezone: 'getTimeZone', + scaninfo: 'getScanInfo', + model: 'getModel' + }; + hs100.newHs100Client = function() { + return new Hs100Api.Client(); + }; + + RED.nodes.registerType('hs100', function(config) { RED.nodes.createNode(this, config); var node = this; - var client = new Hs100Api.Client(); - var plug = client.getPlug({host: config.host}); + var client = hs100.newHs100Client(); + var plug = client.getPlug({ host: config.host }); - node.on('input', function (msg) { - if (msg.payload === 'consumption' || msg.topic === 'consumption') { - plug.getConsumption().then(function (data) { - node.send({payload: data}); - }).catch(errorHandler); - } else if (msg.payload === 'on' || msg.topic === 'on') { + node.on('input', function(msg) { + if (msg.payload === 'on' || msg.topic === 'on') { setPowerState(true); } else if (msg.payload === 'off' || msg.topic === 'off') { setPowerState(false); } else { - errorHandler(new Error('Actuation must be one of [on, off, consumption]')); + var actuation = getActuation(msg.payload) || getActuation(msg.topic); + if (actuation) { + plug[actuation.method]() + .then(function(data) { + node.send({ topic: actuation.name, payload: data }); + }) + .catch(errorHandler); + } else { + errorHandler( + new Error( + 'Actuation must be one of on,off,' + + Object.keys(hs100.supportedActuations).toString() + ) + ); + } } }); - node.on('close', function () { + node.on('close', function() { client.socket.close(); }); @@ -57,20 +87,33 @@ module.exports = function (RED) { node.status({ fill: 'orange', shape: on ? 'dot' : 'circle', - text: 'Turning ' + ( on ? 'on' : 'off') + text: 'Turning ' + (on ? 'on' : 'off') }); - plug.setPowerState(on).then(function () { - node.status({ - fill: 'green', - shape: on ? 'dot' : 'circle', - text: on ? 'on' : 'off' - }); - }).catch(errorHandler); + plug + .setPowerState(on) + .then(function() { + node.status({ + fill: 'green', + shape: on ? 'dot' : 'circle', + text: on ? 'on' : 'off' + }); + }) + .catch(errorHandler); + } + + function getActuation(actuation) { + if (actuation) { + var method = hs100.supportedActuations[actuation.toLowerCase()]; + if (method) { + return { name: actuation, method: method }; + } + } + return null; } function errorHandler(err) { node.error(err); - node.status({fill: 'red', shape: 'dot', text: err.message}); + node.status({ fill: 'red', shape: 'dot', text: err.message }); } }); -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index d5e758b..26dc5e0 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,56 @@ { - "name": "node-red-contrib-hs100", - "version": "0.3.0", - "description": "", - "main": "index.js", - "keywords": ["node-red", "tp-link", "tplink", "hs100"], - "devDependencies": { - "chai": "^3.0.0", - "markdown-to-html": "0.0.13", - "mocha": "^3.1.2", - "node-red-contrib-mock-node": "^0.3.0" - }, - "directories": { - "test": "tests" - }, - "scripts": { - "test": "node_modules/.bin/mocha tests/test.js" - }, - "author": "@biddster", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/biddster/node-red-contrib-hs100.git" - }, - "dependencies": { - "fx-hs100-api": "^0.3.0" - }, - "node-red": { - "nodes": { - "hs100": "index.js" + "name": "node-red-contrib-hs100", + "version": "0.4.0", + "description": "", + "main": "index.js", + "keywords": ["node-red", "tp-link", "tplink", "hs100"], + "devDependencies": { + "chai": "^3.0.0", + "eslint": "^4.11.0", + "eslint-config-biddster": "^0.4.0", + "eslint-config-prettier": "^2.8.0", + "lodash": "^4.17.4", + "markdown-to-html": "0.0.13", + "mocha": "^3.1.2", + "node-red-contrib-mock-node": "^0.4.0", + "nyc": "^11.3.0", + "prettier": "^1.8.2" + }, + "directories": { + "test": "tests" + }, + "scripts": { + "test": "nyc --reporter=html node_modules/.bin/mocha -R spec ./tests/test.js" + }, + "author": "@biddster", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/biddster/node-red-contrib-hs100.git" + }, + "dependencies": { + "fx-hs100-api": "^0.3.0" + }, + "node-red": { + "nodes": { + "hs100": "index.js" + } + }, + "eslintConfig": { + "env": { + "es6": true, + "node": true, + "mocha": true + }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "extends": ["eslint-config-biddster/es6-node", "prettier"] + }, + "prettier": { + "singleQuote": true, + "tabWidth": 4, + "printWidth": 96 } - } } diff --git a/tests/test.js b/tests/test.js index d3b209c..6d9866b 100644 --- a/tests/test.js +++ b/tests/test.js @@ -22,31 +22,87 @@ THE SOFTWARE. */ -"use strict"; +('use strict'); var assert = require('chai').assert; var mock = require('node-red-contrib-mock-node'); -var Hs100Api = require('fx-hs100-api'); +var nodeRedModule = require('../index.js'); +var _ = require('lodash'); - -describe('hs100', function () { +describe('hs100', function() { this.timeout(60000); - describe('test', function () { - it('should turn a known socket on and off', function (done) { - var client = new Hs100Api.Client(); - var plug = client.getPlug({host: '192.168.74.82'}); - plug.setPowerState(true); - console.log('Turned it on'); - plug.getInfo().then(function (device) { - console.log(JSON.stringify(device, null, 4)); - setTimeout(function () { - plug.setPowerState(false); - console.log('Turned it off'); - setTimeout(function () { - console.log('Exiting test'); - done(); - }, 10000); - }, 10000); - }); - }); + it('should turn a socket on', function(done) { + var node = newNode(); + node.emit('input', { payload: 'on' }); + setTimeout(function() { + assert.strictEqual(node.status().text, 'on'); + assert.strictEqual(node.status().shape, 'dot'); + node.emit('close'); + done(); + }, 10); + }); + it('should turn a socket off', function(done) { + var node = newNode(); + node.emit('input', { payload: 'off' }); + setTimeout(function() { + assert.strictEqual(node.status().text, 'off'); + assert.strictEqual(node.status().shape, 'circle'); + node.emit('close'); + done(); + }, 10); + }); + it('should emit consumption data', function(done) { + var node = newNode(); + node.emit('input', { payload: 'consumption' }); + setTimeout(function() { + assert.deepEqual(node.sent(0).payload, { mocked: 'getConsumption' }); + assert.strictEqual(node.sent(0).topic, 'consumption'); + node.emit('close'); + done(); + }, 10); + }); + it('should emit sysinfo data', function(done) { + var node = newNode(); + node.emit('input', { topic: 'SysInfo' }); + setTimeout(function() { + assert.deepEqual(node.sent(0).payload, { mocked: 'getSysInfo' }); + assert.strictEqual(node.sent(0).topic, 'SysInfo'); + node.emit('close'); + done(); + }, 10); + }); + it('should handle errors', function(done) { + var node = newNode(); + node.emit('input', { payload: 'wibble' }); + setTimeout(function() { + assert.isNotNull(node.error(0)); + node.emit('close'); + done(); + }, 10); }); }); + +function newNode() { + return mock(nodeRedModule, {}, null, function(module, node) { + module.newHs100Client = function() { + return { + getPlug: function() { + var plug = {}; + _.values(module.supportedActuations).forEach(function(method) { + plug[method] = function() { + return new Promise(function(resolve, reject) { + resolve({ mocked: method }); + }); + }; + }); + plug.setPowerState = function(state) { + return new Promise(function(resolve, reject) { + resolve({ mocked: state }); + }); + }; + return plug; + }, + socket: { close: function() {} } + }; + }; + }); +}