Skip to content

Commit

Permalink
Merge branch 'release/0.4.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
biddster committed Nov 23, 2017
2 parents 0625841 + f87d5b8 commit 7111799
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 88 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ jspm_packages
.yarn-integrity

.idea

.history
.vscode
44 changes: 31 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
87 changes: 65 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,55 +22,98 @@
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();
});

function setPowerState(on) {
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 });
}
});
};
};
83 changes: 53 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
100 changes: 78 additions & 22 deletions tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {} }
};
};
});
}

0 comments on commit 7111799

Please sign in to comment.