Compare commits

18 Commits

Author SHA1 Message Date
Pierre CLÉMENT
fa37eede7c Update README.md 2018-04-17 21:51:57 +02:00
Pierre CLEMENT
8e3a5ccba3 chore(readme): add message on development ending 2018-04-17 21:42:58 +02:00
Pierre CLEMENT
a51edb976d chore(version): add beta flag 2018-04-17 21:35:09 +02:00
Pierre CLEMENT
df64a9521c feat(all): send messages as parts 2018-03-23 23:08:53 +01:00
Pierre CLEMENT
1828e35a4d fix(magnet): change internal model 2018-03-22 23:08:19 +01:00
Pierre CLEMENT
d04f5c1196 doc(main): update documentation and flows 2018-03-21 23:29:36 +01:00
Pierre CLEMENT
c850ca9536 feat(clean): remove sockets 2018-03-21 23:02:27 +01:00
Pierre CLEMENT
c5afb43b47 feat(yeelight): handle yeelights 2018-03-21 22:52:10 +01:00
Pierre CLEMENT
50df65d7fc feat(motion): handle motion sensor 2018-02-26 21:41:34 +01:00
Pierre CLEMENT
b6e311965a feat(gateway): addset light and play sounds 2018-02-21 22:01:31 +01:00
Pierre CLEMENT
7bff50da16 fix(gateway): set password to send write commands 2018-02-20 22:33:47 +01:00
Pierre CLEMENT
c81fb2db33 fix(gateay): fix all nodes and output node
also start setColor of the gateway
2018-02-20 21:36:54 +01:00
Pierre CLEMENT
6536f0b583 fix(all): fix exclude list selection 2018-02-19 22:49:00 +01:00
Pierre CLEMENT
83090c4338 fix(subdevice): filter on sid 2018-02-19 22:36:15 +01:00
Pierre CLEMENT
ee19899d9d fix(subdevices): use name field instead of undefined field in select 2018-02-19 21:49:29 +01:00
Pierre CLEMENT
03af0b8c06 fix(configurator): use internalModel as filter type 2018-02-19 21:10:27 +01:00
Pierre CLEMENT
f4d54d714b refactor(gateway): rewrite the gateway part 2018-02-18 22:30:25 +01:00
Pierre CLEMENT
c1299336cb feat(gateway): move to lumi-aqara
Also add gateway search and subdevices discovery.
Closes #28, closes #27, closes #26, closes #17 and fixes #12
2018-01-23 10:48:48 +01:00
163 changed files with 4871 additions and 1654 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*.iml
.DS_Store
.idea
/node_modules
.log
package-lock.json

View File

@@ -1,7 +1,8 @@
# node-red-contrib-mi-devices
__:warning: I will not go further because I don't use node-red anymore and I don't have the time, but feel free to fork, PR & so on. If you want to maintain this module, feel free to ask.__
This module contains the following nodes to provide easy integration of the Xiaomi devices into node-red.
This module is a fork of [Harald Rietman module, node-red-contrib-xiaomi-devices](https://github.com/hrietman/node-red-contrib-xiaomi-devices)
The following devices are currently supported:
@@ -12,12 +13,11 @@ The following devices are currently supported:
* Button switch
* Aqara smart wireless switch
* Motion sensor
* Power plug (zigbee)
* Power plug (wifi)
* Aqara Motion sensor
* Yeelight White (mono)
* Yeelight RGB
* Yeelight RGB (color)
## Preperation
## Preparation
To interact with the gateway, you need to enable the developer mode, aka LAN mode in the gateway (see below).
@@ -30,6 +30,12 @@ Make sure to check his page for compatible devices.
npm install node-red-contrib-mi-devices
```
### Migrating from v1.X.X
:warning: When I fully rewrote the code, it has been a need to move to other nodes types. So, there is no backward compatibility between 1.X.X and 2.X.X version (thsi is why a v2 has been released..). That also means that you will have to redo all the configurations add replace previous nodes to new ones, sorry for that.
Last thing, before upgrading to v2, you should remove the previous version, to prevent node-red warn about missing nodes (or delete the `.config.json` file in your userDir, but you might also loose your credentials).
## Usage
From the Xiaomi configurator screen add your different devices by selecting the type of device and a readable description. The readable discription is used on the different edit screen of the nodes to easily select the device you associate to the node.
@@ -89,11 +95,9 @@ The lightning icon should be underline un yellow.
## Sources
* [Harald Rietman node-red module](https://github.com/hrietman/node-red-contrib-xiaomi-devices)
* [Domoticz Instructions](https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara))
* [louisZl Gateway Local API](https://github.com/louisZL/lumi-gateway-local-api)
* [Domoticz Gateway Code](https://github.com/domoticz/domoticz/blob/development/hardware/XiaomiGateway.cpp)
* [Node-red UDP nodes](https://github.com/node-red/node-red/blob/master/nodes/core/io/32-udp.js)
* [Yeelight specs](http://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf)
## Credits

113
dist/devices/gateway/Gateway.js vendored Normal file
View File

@@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events = require("events");
const crypto = require("crypto");
const GatewayServer_1 = require("./GatewayServer");
const _1 = require("./");
const Color_1 = require("../../utils/Color");
class Gateway extends events.EventEmitter {
constructor(sid, ip) {
super();
this.sid = sid;
this.ip = ip;
this._subdevices = {};
}
set password(password) {
this._password = password;
}
get key() {
if (!this.lastToken || !this._password)
return null;
var cipher = crypto.createCipheriv('aes-128-cbc', this._password, Gateway.iv);
var key = cipher.update(Buffer.from(this.lastToken), "ascii", "hex");
cipher.final('hex');
return key;
}
handleMessage(msg) {
if (msg.data) {
if (msg.model === "gateway" && msg.sid === this.sid && msg.token) {
this.lastToken = msg.token;
}
}
if (msg.isGetIdListAck()) {
msg.data.forEach((sid) => {
this.read(sid);
});
}
if (msg.isReadAck() || msg.isReport()) {
if (!this._subdevices[msg.sid]) {
for (let SubDeviceClass of [_1.Magnet, _1.Motion, _1.Switch, _1.Weather]) {
if (SubDeviceClass.acceptedModels.indexOf(msg.model) >= 0) {
this._subdevices[msg.sid] = new SubDeviceClass(msg.sid, msg.model);
this._subdevices[msg.sid].on('values-updated', (sidOrMessage) => {
this.emit("subdevice-values-updated", sidOrMessage);
});
this.emit("subdevice-found", msg.sid);
}
}
}
if (this._subdevices[msg.sid]) {
this._subdevices[msg.sid].handleMessage(msg);
}
}
}
getSubdevice(sid) {
return this._subdevices[sid] || null;
}
hasSubdevice(sid) {
return !!this._subdevices[sid];
}
getIdList() {
this.send({
cmd: "get_id_list",
});
}
read(sid) {
this.send({ cmd: "read", sid: sid || this.sid });
}
setLight(brightness, rgb) {
this.send({
cmd: "write",
data: {
rgb: Color_1.Color.toValue(rgb.red, rgb.green, rgb.blue, brightness),
sid: this.sid
}
});
}
playSound(musicId, volume) {
this.send({
cmd: "write",
data: {
mid: musicId,
volume: volume,
sid: this.sid
}
});
}
send(message) {
let msg = Object.assign({}, message.payload || message);
if (msg.cmd) {
msg.sid = message.sid || this.sid;
if (msg.gateway) {
delete msg.gateway;
}
if (msg.cmd === "write") {
msg.data.key = this.key;
}
GatewayServer_1.GatewayServer.getInstance().sendToGateway(this.sid, msg);
}
}
get subdevices() {
return this._subdevices;
}
toJSON() {
return {
sid: this.sid,
ip: this.ip,
key: this.password,
subdevices: this.subdevices
};
}
}
Gateway.iv = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
exports.Gateway = Gateway;

30
dist/devices/gateway/GatewayMessage.js vendored Normal file
View File

@@ -0,0 +1,30 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class GatewayMessage {
constructor(raw) {
Object.assign(this, raw);
if (raw.port) {
this.port = parseInt(raw.port);
}
if (raw.data) {
this.data = JSON.parse(raw.data) || raw.data;
}
this.timestamp = +new Date;
}
isHeartbeat() {
return this.cmd === "heartbeat";
}
isIam() {
return this.cmd === "iam";
}
isGetIdListAck() {
return this.cmd === "get_id_list_ack";
}
isReadAck() {
return this.cmd === "read_ack";
}
isReport() {
return this.cmd === "report";
}
}
exports.GatewayMessage = GatewayMessage;

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

121
dist/devices/gateway/GatewayServer.js vendored Normal file
View File

@@ -0,0 +1,121 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events = require("events");
const dgram = require("dgram");
const _1 = require("./");
const GatewayMessage_1 = require("./GatewayMessage");
class GatewayServer extends events.EventEmitter {
constructor() {
super(...arguments);
this._gateways = {};
this._gatewaysPing = {};
}
static getInstance() {
if (!this.instance) {
this.instance = new GatewayServer();
}
return this.instance;
}
discover(ipv = 4) {
if (this.server) {
return;
}
this.server = dgram.createSocket({
type: `udp${ipv}`,
reuseAddr: true
});
this.server.on('listening', () => {
var address = this.server.address();
//this.log(RED._("udp.status.listener-at",{host:address.address,port:address.port}));
this.server.setBroadcast(true);
try {
this.server.setMulticastTTL(128);
this.server.addMembership(GatewayServer.MULTICAST_ADDRESS, null);
}
catch (e) {
/*if (e.errno == "EINVAL") {
this.error(RED._("udp.errors.bad-mcaddress"));
} else if (e.errno == "ENODEV") {
this.error(RED._("udp.errors.interface"));
} else {
this.error(RED._("udp.errors.error",{error:e.errno}));
}*/
}
});
this.server.on("error", (err) => {
/*if ((err.code == "EACCES") && (this.port < 1024)) {
this.error(RED._("udp.errors.access-error"));
} else {
this.error(RED._("udp.errors.error",{error:err.code}));
}*/
this.server.close();
delete this.server;
});
this.server.on('message', (message, remote) => {
let msg = new GatewayMessage_1.GatewayMessage(JSON.parse(message.toString('utf8')));
//console.log(msg);
let gatewaySid = null;
if ((msg.isHeartbeat() || msg.isIam()) && msg.model === "gateway") {
if (!this._gateways[msg.sid]) {
this._gateways[msg.sid] = new _1.Gateway(msg.sid, remote.address);
this._gateways[msg.sid].getIdList();
this.emit("gateway-online", msg.sid);
}
else {
// Any IP update?
this._gateways[msg.sid].ip = remote.address;
}
if (this._gatewaysPing[msg.sid]) {
clearTimeout(this._gatewaysPing[msg.sid]);
delete this._gatewaysPing[msg.sid];
}
// Consider the gateway as unreachable after 2 heartbeats missed (1 heartbeat every 10s)
this._gatewaysPing[msg.sid] = setTimeout(() => {
this.emit("gateway-offline", msg.sid);
delete this._gateways[msg.sid];
}, 25 * 1000);
gatewaySid = msg.sid;
}
if (!gatewaySid) {
gatewaySid = Object.keys(this._gateways).filter((gatewaySid) => this._gateways[gatewaySid].ip === remote.address)[0];
}
gatewaySid && this._gateways[gatewaySid] && this._gateways[gatewaySid].handleMessage(msg);
});
return new Promise((resolve, reject) => {
try {
this.server.bind(GatewayServer.SERVER_PORT, null);
let msg = Buffer.from(JSON.stringify({ cmd: "whois" }));
this.server.send(msg, 0, msg.length, GatewayServer.MULTICAST_PORT, GatewayServer.MULTICAST_ADDRESS);
resolve(this.server);
}
catch (e) {
reject();
}
});
}
stop() {
if (this.server) {
this.server.close();
delete this.server;
}
}
getGateway(sid) {
return this._gateways[sid] || null;
}
hasGateway(sid) {
return !!this._gateways[sid];
}
get gateways() {
return this._gateways;
}
sendToGateway(sid, message) {
if (this.server && this._gateways[sid]) {
let msg = Buffer.from(JSON.stringify(message));
this.server.send(msg, 0, msg.length, GatewayServer.SERVER_PORT, this._gateways[sid].ip);
}
}
}
GatewayServer.MULTICAST_ADDRESS = '224.0.0.50';
GatewayServer.MULTICAST_PORT = 4321;
GatewayServer.SERVER_PORT = 9898;
exports.GatewayServer = GatewayServer;

View File

@@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events = require("events");
class GatewaySubdevice extends events.EventEmitter {
constructor(sid, model) {
super();
this.sid = sid;
this.model = model;
}
get batteryLevel() {
/*
When full, CR2032 batteries are between 3 and 3.4V
http://farnell.com/datasheets/1496885.pdf
*/
return this.voltage ? Math.min(Math.round((this.voltage - 2200) / 10), 100) : -1;
}
handleMessage(msg) {
if (msg.data.voltage) {
this.voltage = msg.data.voltage;
}
this.message = msg;
}
static get acceptedModels() {
return [];
}
;
toJSON() {
let json = {};
for (let prop of Object.keys(this)) {
json[prop] = this[prop];
}
delete json._events;
delete json._eventsCount;
delete json._maxListeners;
json.batteryLevel = this.batteryLevel;
json.internalModel = this.internalModel;
return json;
}
}
exports.GatewaySubdevice = GatewaySubdevice;

32
dist/devices/gateway/Magnet.js vendored Normal file
View File

@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const GatewaySubdevice_1 = require("./GatewaySubdevice");
class Magnet extends GatewaySubdevice_1.GatewaySubdevice {
static get acceptedModels() {
return ['magnet', 'sensor_magnet.aq2'];
}
get internalModel() {
return 'mi.magnet';
}
isClosed() {
return this.status === "close";
}
isOpened() {
return this.status === "open";
}
isUnkownState() {
return this.status === "unkown";
}
handleMessage(msg) {
super.handleMessage(msg);
if (msg.isReadAck() || msg.isReport()) {
let data = msg.data;
// TODO: mintime
if (this.status !== data.status) {
this.status = data.status;
this.emit('values-updated', this.sid);
}
}
}
}
exports.Magnet = Magnet;

29
dist/devices/gateway/Motion.js vendored Normal file
View File

@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const GatewaySubdevice_1 = require("./GatewaySubdevice");
class Motion extends GatewaySubdevice_1.GatewaySubdevice {
constructor() {
super(...arguments);
this.lux = 0;
}
static get acceptedModels() {
return ['motion', 'sensor_motion.aq2'];
}
get internalModel() {
return 'mi.motion';
}
handleMessage(msg) {
super.handleMessage(msg);
if (msg.isReadAck() || msg.isReport()) {
let data = msg.data;
if (data.lux) {
this.lux = parseInt(data.lux);
}
if (data.status === "motion") {
this.lastMotionTimestamp = data.timestamp;
this.emit('values-updated', { sid: this.sid, data: { hasMotion: true } });
}
}
}
}
exports.Motion = Motion;

15
dist/devices/gateway/Switch.js vendored Normal file
View File

@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const GatewaySubdevice_1 = require("./GatewaySubdevice");
class Switch extends GatewaySubdevice_1.GatewaySubdevice {
static get acceptedModels() {
return ['switch', 'sensor_switch.aq2'];
}
get internalModel() {
return 'mi.switch';
}
handleMessage(msg) {
super.handleMessage(msg);
}
}
exports.Switch = Switch;

36
dist/devices/gateway/Weather.js vendored Normal file
View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const GatewaySubdevice_1 = require("./GatewaySubdevice");
class Weather extends GatewaySubdevice_1.GatewaySubdevice {
static get acceptedModels() {
return ['sensor_ht', 'weather.v1'];
}
get internalModel() {
return 'mi.weather';
}
get temperatureInDegrees() {
return this.temperature / 100;
}
get humidityInPercent() {
return this.humidity / 100;
}
get pressureInBar() {
return this.pressure / 100000;
}
get pressureInhPa() {
return this.pressure / 100;
}
handleMessage(msg) {
super.handleMessage(msg);
if (msg.isReadAck() || msg.isReport()) {
let data = msg.data;
['temperature', 'humidity', 'pressure'].forEach((dataType) => {
if (data[dataType]) {
this[dataType] = parseInt(data[dataType]);
}
});
this.emit('values-updated', this.sid);
}
}
}
exports.Weather = Weather;

14
dist/devices/gateway/index.js vendored Normal file
View File

@@ -0,0 +1,14 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./"));
__export(require("./Gateway"));
__export(require("./GatewayMessage"));
__export(require("./GatewayServer"));
__export(require("./GatewaySubdevice"));
__export(require("./Magnet"));
__export(require("./Motion"));
__export(require("./Switch"));
__export(require("./Weather"));

45
dist/devices/yeelight/YeelightServer.js vendored Normal file
View File

@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events = require("events");
const YeelightSearch = require("yeelight-wifi");
class YeelightServer extends events.EventEmitter {
constructor() {
super(...arguments);
this._bulbs = {};
this._bulbsJson = {};
}
static getInstance() {
if (!this.instance) {
this.instance = new YeelightServer();
}
return this.instance;
}
get bulbs() {
return this._bulbsJson;
}
getBulb(sid) {
return this._bulbs[sid];
}
discover() {
new Promise(() => {
(new YeelightSearch()).on('found', (bulb) => {
bulb.sid = parseInt(bulb.id);
if (!this._bulbs[bulb.sid]) {
this._bulbs[bulb.sid] = bulb;
this._bulbsJson[bulb.sid] = YeelightServer.bulbToJSON(bulb);
this.emit("yeelight-online", bulb.sid);
}
});
});
// TODO: disconected ?
}
static bulbToJSON(bulb) {
return {
sid: bulb.sid,
ip: bulb.hostname,
name: bulb.name,
model: bulb.model
};
}
}
exports.YeelightServer = YeelightServer;

0
dist/devices/yeelight/index.js vendored Normal file
View File

26
dist/nodes/actions/GatewayPlaySound.js vendored Normal file
View File

@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class GatewayPlaySound {
constructor(props) {
RED.nodes.createNode(this, props);
this.mid = parseInt(props.mid);
this.volume = parseInt(props.volume);
this.setListeners();
}
setListeners() {
this.on('input', (msg) => {
if (msg.sid) {
msg.payload = {
action: "playSound",
mid: msg.mid || this.mid,
volume: msg.volume || this.volume
};
}
this.send(msg);
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions gateway_play_sound`, GatewayPlaySound);
};

23
dist/nodes/actions/GatewayStopSound.js vendored Normal file
View File

@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class GatewayStopSound {
constructor(props) {
RED.nodes.createNode(this, props);
this.setListeners();
}
setListeners() {
this.on('input', (msg) => {
if (msg.sid) {
msg.payload = {
action: "playSound",
mid: 1000
};
}
this.send(msg);
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions gateway_stop_sound`, GatewayStopSound);
};

24
dist/nodes/actions/Light.js vendored Normal file
View File

@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class Light {
constructor(props) {
RED.nodes.createNode(this, props);
this.color = props.color;
this.brightness = props.brightness;
this.setListeners();
}
setListeners() {
this.on('input', (msg) => {
msg.payload = {
action: "setLight",
color: msg.color || this.color,
brightness: msg.brightness || this.brightness
};
this.send(msg);
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions light`, Light);
};

20
dist/nodes/actions/ReadAction.js vendored Normal file
View File

@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED, type) => {
class ReadAction {
constructor(props) {
RED.nodes.createNode(this, props);
this.on('input', (msg) => {
if (msg.sid) {
msg.payload = {
cmd: this.type.replace(`${constants_1.Constants.NODES_PREFIX}-actions `, ''),
sid: msg.sid
};
this.send(msg);
}
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions ${type}`, ReadAction);
};

18
dist/nodes/actions/ToggleAction.js vendored Normal file
View File

@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED, action) => {
class ToggleAction {
constructor(props) {
RED.nodes.createNode(this, props);
this.setListeners();
}
setListeners() {
this.on('input', (msg) => {
msg.payload = { action };
this.send(msg);
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions ${action}`, ToggleAction);
};

23
dist/nodes/actions/WriteAction.js vendored Normal file
View File

@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED, type) => {
class WriteAction {
constructor(props) {
RED.nodes.createNode(this, props);
this.on('input', (msg) => {
if (msg.sid) {
msg.payload = {
cmd: "write",
data: {
status: this.type.replace(`${constants_1.Constants.NODES_PREFIX}-actions `, ''),
sid: msg.sid
}
};
this.send(msg);
}
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions ${type}`, WriteAction);
};

View File

Before

Width:  |  Height:  |  Size: 1016 B

After

Width:  |  Height:  |  Size: 1016 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 601 B

After

Width:  |  Height:  |  Size: 601 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 990 B

After

Width:  |  Height:  |  Size: 990 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,30 +1,29 @@
<!-- The Read Node -->
<script type="text/x-red" data-template-name="xiaomi-actions read">
<script type="text/x-red" data-template-name="mi-devices-actions read">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions read">
<script type="text/x-red" data-help-name="mi-devices-actions read">
<p>Ask the gateway to read the report of the input device.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid
<span class="property-type">string</span>
</dt>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
</dl>
<h3>Outputs</h3>
<p class="node-ports">
Message to connect to a gateway out node.
</p>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions read',{
RED.nodes.registerType('mi-devices-actions read',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -35,31 +34,37 @@
paletteLabel: "read",
icon: "mi-read.png",
label: function() {
return this.name||"read";
return this.name||"mi-devices read";
}
});
</script>
<!-- The get ids Node -->
<script type="text/x-red" data-template-name="xiaomi-actions get_id_list">
<script type="text/x-red" data-template-name="mi-devices-actions get_id_list">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions get_id_list">
<script type="text/x-red" data-help-name="mi-devices-actions get_id_list">
<p>Ask the gateway to the list of devices ids.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions get_id_list',{
RED.nodes.registerType('mi-devices-actions get_id_list',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -70,44 +75,37 @@
paletteLabel: "get id list",
icon: "mi-list.png",
label: function() {
return this.name||"get id list";
return this.name||"mi-devices get id list";
}
});
</script>
<!-- The Single click Node -->
<script type="text/x-red" data-template-name="xiaomi-actions click">
<script type="text/x-red" data-template-name="mi-devices-actions click">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions click">
<script type="text/x-red" data-help-name="mi-devices-actions click">
<p>Virtual single click for switch.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid
<span class="property-type">string</span>
</dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway
<span class="property-type">xiaomi-configurator</span>
</dt>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions click',{
RED.nodes.registerType('mi-devices-actions click',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -118,43 +116,37 @@
paletteLabel: "click",
icon: "mi-click.png",
label: function() {
return this.name||"click";
return this.name||"mi-devices click";
}
});
</script>
<!-- The Double click Node -->
<script type="text/x-red" data-template-name="xiaomi-actions double_click">
<script type="text/x-red" data-template-name="mi-devices-actions double_click">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions double_click">
<script type="text/x-red" data-help-name="mi-devices-actions double_click">
<p>Virtual double click for switch.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid
<span class="property-type">string</span>
</dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway
<span class="property-type">xiaomi-configurator</span>
</dt>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions double_click',{
RED.nodes.registerType('mi-devices-actions double_click',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -163,17 +155,16 @@
inputs:1,
outputs:1,
paletteLabel: "double click",
icon: "mi-double-click.png",
icon: "double-click.png",
label: function() {
return this.name||"double click";
return this.name||"mi-devices double click";
}
});
</script>
<!-- The Gateway light Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions gateway_light', {
RED.nodes.registerType('mi-devices-actions light', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -203,7 +194,7 @@
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions gateway_light">
<script type="text/x-red" data-template-name="mi-devices-actions light">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
@@ -218,7 +209,7 @@
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions gateway_light">
<script type="text/x-red" data-help-name="mi-devices-actions light">
<p>Change the light of the gateway.</p>
<h3>Inputs</h3>
@@ -250,11 +241,9 @@
<li>Message to connect to a gateway out node.</li>
</ol>
</script>
<!-- The Gateway sound Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions gateway_sound', {
RED.nodes.registerType('mi-devices-actions gateway_play_sound', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -272,7 +261,7 @@
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions gateway_sound">
<script type="text/x-red" data-template-name="mi-devices-actions gateway_play_sound">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
@@ -309,9 +298,10 @@
<label for="node-input-volume"><i class="icon-tag"></i> Volume</label>
<input type="range" id="node-input-volume" min="0" max="100">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions gateway_sound">
<script type="text/x-red" data-help-name="mi-devices-actions gateway_play_sound">
<p>Play a sound on the gateway.</p>
<h3>Inputs</h3>
@@ -330,34 +320,34 @@
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
</script>
<!-- The Gateway stop sound Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions gateway_stop_sound',{
RED.nodes.registerType('mi-devices-actions gateway_stop_sound', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
name: {value: ""}
},
inputs:1,
outputs:1,
inputs: 1,
outputs: 1,
paletteLabel: "stop sound",
icon: "mi-mute.png",
label: function() {
return this.name||"stop sound";
label: function () {
return this.name || "stop sound";
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions gateway_stop_sound">
<script type="text/x-red" data-template-name="mi-devices-actions gateway_stop_sound">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions gateway_stop_sound">
<script type="text/x-red" data-help-name="mi-devices-actions gateway_stop_sound">
<p>
Stop current playing sound on the gateway.
</p>
@@ -366,12 +356,35 @@
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
</script>
<!-- The "on" Node -->
<script type="text/x-red" data-template-name="mi-devices-actions turn_on">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-actions turn_on">
<p>Turn device on.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
</dl>
<h3>Outputs</h3>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions on',{
RED.nodes.registerType('mi-devices-actions turn_on',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -379,35 +392,40 @@
},
inputs:1,
outputs:1,
paletteLabel: "on",
paletteLabel: "turn on",
icon: "mi-on.png",
label: function() {
return this.name||"power on";
return this.name||"mi-devices turn on";
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions on">
<script type="text/x-red" data-template-name="mi-devices-actions turn_off">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions on">
<p>
Turn input device to on.
</p>
<script type="text/x-red" data-help-name="mi-devices-actions turn_off">
<p>Turn device off.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway/yeelight out node.</li>
</ol>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<!-- The "off" Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions off',{
RED.nodes.registerType('mi-devices-actions turn_off',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -415,35 +433,40 @@
},
inputs:1,
outputs:1,
paletteLabel: "off",
paletteLabel: "turn off",
icon: "mi-off.png",
label: function() {
return this.name||"power off";
return this.name||"mi-devices turn off";
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions off">
<script type="text/x-red" data-template-name="mi-devices-actions toggle">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions off">
<p>
Turn input device to off.
</p>
<script type="text/x-red" data-help-name="mi-devices-actions toggle">
<p>Toggle device.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway/yeelight out node.</li>
</ol>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<!-- The "toggle" Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions toggle',{
RED.nodes.registerType('mi-devices-actions toggle',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -454,22 +477,7 @@
paletteLabel: "toggle",
icon: "mi-toggle.png",
label: function() {
return this.name||"toggle power";
return this.name||"mi-devices toggle";
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions toggle">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-actions toggle">
<p>Toggle device.</p>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway/yeelight out node.</li>
</ol>
</script>
</script>

21
dist/nodes/actions/index.js vendored Normal file
View File

@@ -0,0 +1,21 @@
"use strict";
const ReadAction_1 = require("./ReadAction");
const WriteAction_1 = require("./WriteAction");
const Light_1 = require("./Light");
const GatewayPlaySound_1 = require("./GatewayPlaySound");
const GatewayStopSound_1 = require("./GatewayStopSound");
const ToggleAction_1 = require("./ToggleAction");
module.exports = (RED) => {
["read", "get_id_list"].forEach((action) => {
ReadAction_1.default(RED, action);
});
["click", "double_click"].forEach((action) => {
WriteAction_1.default(RED, action);
});
Light_1.default(RED);
GatewayPlaySound_1.default(RED);
GatewayStopSound_1.default(RED);
["turn_on", "turn_off", "toggle"].forEach(action => {
ToggleAction_1.default(RED, action);
});
};

10
dist/nodes/constants.js vendored Normal file
View File

@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Constants {
static get NODES_PREFIX() {
let packageJson = require(`${__dirname}/../../package`);
return packageJson.config.nodes_prefix;
}
;
}
exports.Constants = Constants;

67
dist/nodes/gateway-subdevices/All.js vendored Normal file
View File

@@ -0,0 +1,67 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const uniqid = require("uniqid");
exports.default = (RED) => {
class All {
static getOnlyModelsValue(input) {
var cleanOnlyModels = [];
input.forEach((value) => {
cleanOnlyModels = cleanOnlyModels.concat(value.split(','));
});
return cleanOnlyModels;
}
constructor(props) {
RED.nodes.createNode(this, props);
this.gatewayConf = RED.nodes.getNode(props.gateway);
this.onlyModels = All.getOnlyModelsValue(props.onlyModels || []);
this.excludedSids = props.excludedSids;
this.setMessageListener();
}
setMessageListener() {
this.on('input', (msg) => {
if (this.gatewayConf) {
// Filter input
if (msg.payload && msg.payload.model && msg.payload.sid) {
if (!this.isDeviceValid(msg.payload.sid)) {
msg = null;
}
this.send(msg);
}
else {
let partsId = uniqid();
Object.keys(this.gatewayConf.deviceList || {})
.filter((sid) => this.isDeviceValid(sid))
.forEach((sid, i, subSids) => {
let curMsg = Object.assign({}, msg);
delete curMsg._msgid;
curMsg.parts = {
id: partsId,
index: i,
count: subSids.length,
};
curMsg.sid = sid;
curMsg.gateway = this.gatewayConf;
this.send(curMsg);
});
}
}
});
}
isDeviceValid(sid) {
if ((!this.onlyModels || this.onlyModels.length == 0) && (!this.excludedSids || this.excludedSids.length == 0)) {
return true;
}
let device = this.gatewayConf.deviceList[sid];
// Is excluded
if ((this.excludedSids && this.excludedSids.length != 0) && this.excludedSids.indexOf(sid) >= 0) {
return false;
}
if ((this.onlyModels && this.onlyModels.length != 0) && this.onlyModels.indexOf(device.internalModel) >= 0) {
return true;
}
return false;
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-all`, All);
};

View File

@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED, type) => {
class GatewayDevice {
constructor(props) {
RED.nodes.createNode(this, props);
this.gateway = RED.nodes.getNode(props.gateway);
this.sid = props.sid;
this.status({ fill: "grey", shape: "ring", text: "battery - na" });
this.setMessageListener();
}
setMessageListener() {
if (this.gateway) {
this.on('input', (msg) => {
let payload = msg.payload;
// Input from gateway
if (payload.sid) {
if (payload.sid == this.sid) {
let batteryLevel = payload.batteryLevel;
var status = {
fill: "green", shape: "dot",
text: "battery - " + batteryLevel + "%"
};
if (batteryLevel < 10) {
status.fill = "red";
}
else if (batteryLevel < 45) {
status.fill = "yellow";
}
this.status(status);
this.send([msg]);
}
}
else {
msg.sid = this.sid;
msg.gateway = this.gateway;
this.send(msg);
}
});
}
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-mi.${type}`, GatewayDevice);
};

34
dist/nodes/gateway-subdevices/Plug.js vendored Normal file
View File

@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class Plug {
constructor(props) {
RED.nodes.createNode(this, props);
this.gateway = RED.nodes.getNode(props.gateway);
this.status({ fill: "grey", shape: "ring", text: "status" });
}
setListener() {
if (this.gateway) {
this.on('input', (msg) => {
var payload = msg.payload;
if (payload.sid) {
if (payload.sid == this.sid) {
if (payload.data.status && payload.data.status == "on") {
this.status({ fill: "green", shape: "dot", text: "on" });
}
else if (payload.data.status && payload.data.status == "off") {
this.status({ fill: "red", shape: "dot", text: "off" });
}
this.send(msg);
}
}
else {
this.send(msg);
}
});
}
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-plug`, Plug);
};

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 842 B

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

729
dist/nodes/gateway-subdevices/index.html vendored Normal file
View File

@@ -0,0 +1,729 @@
<script type="text/javascript">
RED.nodes.registerType('mi-devices-all', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"mi-devices-gateway configurator"},
name: {value: ""},
onlyModels: {value: []},
excludedSids: { value: []}
},
inputs: 1,
outputs: 1,
outputLabels: ["All devices"],
paletteLabel: "all",
icon: "mi-all.png",
label: function () {
return this.name || "xiaomi-all";
},
oneditprepare: function() {
var node = this;
function getOnlyModelsValue(input) {
var cleanOnlyModels = [];
input.forEach(function(value) {
cleanOnlyModels = cleanOnlyModels.concat(value.split(','));
});
return cleanOnlyModels;
}
function changeGateway(gateway, onlyModels, excludedSids) {
var configNodeID = gateway || $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
onlyModels = getOnlyModelsValue(onlyModels || $('#node-input-onlyModels').val() || []);
excludedSids = excludedSids || $('#node-input-excludedSids').val() || [];
$('#node-input-excludedSids').empty();
for (sid in configNode.deviceList) {
var device = configNode.deviceList[sid];
if (onlyModels.length == 0 || onlyModels.indexOf(device.internalModel) >= 0) {
var option = $('<option value="' + sid + '">' + device.name + '</option>');
if(excludedSids && excludedSids.indexOf(sid) >= 0) {
option.prop('selected', true);
}
$('#node-input-excludedSids').append(option);
}
}
}
}
}
changeGateway(this.gateway, this.onlyModels, this.excludedSids);
$("#node-input-gateway, #node-input-onlyModels").change(function () {
changeGateway();
});
},
oneditsave: function() {
if(!$('#node-input-onlyModels').val()) {
this.onlyModels = [];
}
if(!$('#node-input-excludedSids').val()) {
this.excludedSids = [];
}
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-all">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<hr />
<h5>Filters</h5>
<div class="form-row">
<label for="node-input-onlyModels"><i class="icon-tag"></i> Only</label>
<select multiple id="node-input-onlyModels">
<option value="mi.weather">Temperature/humidty</option>
<option value="mi.motion">Motion</option>
<option value="mi.switch">Switches</option>
<option value="mi.magnet">Magnets</option>
<option value="mi.plug">Plugs</option>
</select>
</div>
<div class="form-row">
<label for="node-input-excludedSids"><i class="icon-tag"></i> Exclude</label>
<select multiple id="node-input-excludedSids" size=10></select>
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-all">
<p>All devices registred in the gateway, except gateway itself.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>When use as an incoming filter node, <code>sid</code> and <code>model</code> are mandatory.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Devices output
<dl class="message-properties">
<dt>payload <span class="property-type">array</span></dt>
<dd>Array of devices.</dd>
</dl>
</li>
</ol>
<h4>Details</h4>
<p>Sample payload:</p>
<p><pre>[
{sid: "128d0901db1fa8", desc: "Door sensor" model: "magnet"},
{sid: "151d0401ab2491", desc: "Heat sensor", model: "sensor_ht"},
{sid: "658d030171427c", desc: "Button", model: "switch"}
]</pre>
</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-plug', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"mi-devices-gateway configurator"},
name: {value: ""},
sid: {value: "", required: true},
onmsg: {value: ""},
offmsg: {value: ""},
output: {value: "0"}
},
inputs: 1,
outputs: 1,
paletteLabel: "plug (zigbee)",
icon: "outlet-icon.png",
label: function () {
return this.name || "xiaomi-plug";
},
oneditprepare: function() {
var node = this;
if(node.sid) {
$('#node-input-sid').val(node.sid);
}
function changeGateway(model) {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
$('#node-input-sid').empty();
for (key in configNode.deviceList) {
var device = configNode.deviceList[key];
if (device.model === model) {
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + '</option>');
}
}
if(node.sid) {
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
}
}
}
}
$("#node-input-sid").change(function () {
if(!this.name) {
$("#node-input-name").val($('#node-input-sid option:selected').text());
}
});
$("#node-input-gateway").change(function () {
changeGateway("plug");
});
},
oneditsave: function() {
var node = this;
node.sid = $("#node-input-sid").val();
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-plug">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-plug">
<p>The Xiaomi plug (zigbee) node</p>
<p>This is the plug (socket) version which is attached to a Xiaomi gateway. The Wifi version is not yet supported.</p>
<p>To switch an output you need to specify the key of the gateway in the gateway configuration; without the key
no output can be switched. To retrieve the gateway key consult the Xiaomi Mi Home App.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">string | json</span>
</dt>
<dd>When the node is used as filter, gateway <code>plug</code> message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>. Or <code>on</code> or <code>off</code>.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Status output
<dl class="message-properties">
<dt>payload <span class="property-type">string | json</span></dt>
<dd>raw data, value or template.</dd>
</dl>
</li>
<li>Control output
<dl class="message-properties">
<dt>payload <span class="property-type">json</span></dt>
<dd>Gateway <code>write_cmd</code> to switch the output on or off.</dd>
</dl>
</li>
</ol>
<h4>Details</h4>
<p>The incoming json message is parsed if the type model is <code>plug</code> and
the <code>sid</code> matches the configured value for this device.</p>
<p>On the input you can send the string <code>on</code> to switch the plug on. To turn it off just send the string <code>off</code></p>
<p>Sample message:</p>
<p><pre>{
cmd: "write_ack"
model: "plug"
sid: "158d00012f1fb5"
short_id: 47414
data: {
voltage:3600,
status:"off",
inuse:"0",
power_consumed:"4000",
load_power:"0"
}
}</pre></p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-mi.magnet', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"mi-devices-gateway configurator"},
name: {value: ""},
sid: {value: "", required: true}
},
inputs: 1,
outputs: 1,
paletteLabel: "magnet",
icon: "door-icon.png",
label: function () {
return this.name || "mi-devices mi.magnet";
},
oneditprepare: function() {
var node = this;
if(node.sid) {
$('#node-input-sid').val(node.sid);
}
function changeGateway(model) {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
$('#node-input-sid').empty();
for (sid in configNode.deviceList) {
var device = configNode.deviceList[sid];
if (device.internalModel === model) {
$('#node-input-sid').append('<option value="' + sid + '">' + device.name + ' - ' + sid + '</option>');
}
}
if(node.sid) {
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
}
}
}
}
$("#node-input-sid").change(function () {
if(!this.name) {
$("#node-input-name").val($('#node-input-sid option:selected').text());
}
});
$("#node-input-gateway").change(function () {
changeGateway("mi.magnet");
});
},
oneditsave: function() {
var node = this;
node.sid = $("#node-input-sid").val();
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-mi.magnet">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-mi.magnet">
<p>The Xiaomi contact sensor node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample payload after incoming incoming message:</p>
<p><pre>{
cmd: "read_ack"
model: "magnet"
sid: "158d000112fb5d"
short_id: 50301
data: {
voltage: 3015,
status: "close",
batteryLevel: 23
}
}</pre>
Where &lt;code&gt;status&lt;/code&gt; can be &lt;code&gt;&#34;open&#34;&lt;/code&gt; or &lt;code&gt;&#34;close&#34;&lt;/code&gt;, <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-mi.motion', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"mi-devices-gateway configurator"},
name: {value: ""},
sid: {value: "", required: true}
},
inputs: 1,
outputs: 1,
paletteLabel: "motion",
icon: "motion-icon.png",
label: function () {
return this.name || "mi-devices mi.motion";
},
oneditprepare: function() {
var node = this;
if(node.sid) {
$('#node-input-sid').val(node.sid);
}
function changeGateway(model) {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
$('#node-input-sid').empty();
for (sid in configNode.deviceList) {
var device = configNode.deviceList[sid];
if (device.internalModel === model) {
$('#node-input-sid').append('<option value="' + sid + '">' + device.name + ' - ' + sid + '</option>');
}
}
if(node.sid) {
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
}
}
}
}
$("#node-input-sid").change(function () {
if(!this.name) {
$("#node-input-name").val($('#node-input-sid option:selected').text());
}
});
$("#node-input-gateway").change(function () {
changeGateway("mi.motion");
});
},
oneditsave: function() {
var node = this;
node.sid = $("#node-input-sid").val();
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-mi.motion">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-mi.motion">
<p>The Xiaomi body motion sensor node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample payload after incoming incoming message:</p>
<p><pre>{
cmd: "read_ack"
model: "motion"
sid: "158d00015ef56c"
short_id: 21672
data: {
voltage: 3035,
status: "motion",
batteryLevel: 45
}
}</pre>
Where <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-mi.weather', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"mi-devices-gateway configurator"},
name: {value: ""},
sid: {value: "", required: true}
},
inputs: 1,
outputs: 1,
paletteLabel: "weather",
icon: "thermometer-icon.png",
label: function () {
return this.name || "mi-devices mi.weather";
},
oneditprepare: function() {
var node = this;
if(node.sid) {
$('#node-input-sid').val(node.sid);
}
function changeGateway(model) {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
$('#node-input-sid').empty();
for (sid in configNode.deviceList) {
var device = configNode.deviceList[sid];
if (device.internalModel === model) {
$('#node-input-sid').append('<option value="' + sid + '">' + device.name + ' - ' + sid + '</option>');
}
}
if(node.sid) {
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
}
}
}
}
$("#node-input-sid").change(function () {
if(!this.name) {
$("#node-input-name").val($('#node-input-sid option:selected').text());
}
});
$("#node-input-gateway").change(function () {
changeGateway("mi.weather");
});
},
oneditsave: function() {
var node = this;
node.sid = $("#node-input-sid").val();
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-mi.weather">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-mi.weather">
<p>The Xiaomi Humidity &amp; Temperature sensor node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample payload after incoming incoming message:</p>
<p><pre>{
cmd: "read_ack"
model: "weather.v1"
sid: "158d00010b7f1b"
short_id: 8451
data: {
voltage:3005,
temperature:23.25,
humidity:56.99,
pressure:981.26,
batteryLevel: 34
}
}</pre>
Where &lt;code&gt;humidy&lt;/code&gt; is in percents, &lt;code&gt;pressure&lt;/code&gt; in kPa, <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-mi.switch', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"mi-devices-gateway configurator"},
name: {value: ""},
sid: {value: "", required: true}
},
inputs: 1,
outputs: 1,
paletteLabel: "switch",
icon: "mi-switch.png",
label: function () {
return this.name || "mi-devices mi.switch";
},
oneditprepare: function() {
var node = this;
if(node.sid) {
$('#node-input-sid').val(node.sid);
}
function changeGateway(model) {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
$('#node-input-sid').empty();
for (sid in configNode.deviceList) {
var device = configNode.deviceList[sid];
if (device.internalModel === model) {
$('#node-input-sid').append('<option value="' + sid + '">' + device.name + ' - ' + sid + '</option>');
}
}
if(node.sid) {
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
}
}
}
}
$("#node-input-sid").change(function () {
if(!this.name) {
$("#node-input-name").val($('#node-input-sid option:selected').text());
}
});
$("#node-input-gateway").change(function () {
changeGateway("mi.switch");
});
},
oneditsave: function() {
var node = this;
node.sid = $("#node-input-sid").val();
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-mi.switch">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-mi.switch">
<p>The Xiaomi Switch sensor node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>mi-devices-gateway configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample payload after incoming incoming message:</p>
<p><pre>{
cmd: "report"
model: "switch"
sid: "158d000128b124"
short_id: 56773
data: {
status: "click",
batteryLevel: 23
}
}</pre>
Where <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
</script>

11
dist/nodes/gateway-subdevices/index.js vendored Normal file
View File

@@ -0,0 +1,11 @@
"use strict";
const All_1 = require("./All");
const Plug_1 = require("./Plug");
const GatewaySubdevice_1 = require("./GatewaySubdevice");
module.exports = (RED) => {
All_1.default(RED);
Plug_1.default(RED);
["magnet", "motion", "sensor", "switch"].forEach((subdeviceType) => {
GatewaySubdevice_1.default(RED, subdeviceType);
});
};

46
dist/nodes/gateway/Gateway.js vendored Normal file
View File

@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class Gateway {
constructor(props) {
RED.nodes.createNode(this, props);
this.gatewayConf = RED.nodes.getNode(props.gateway);
this.status({ fill: "red", shape: "ring", text: "offline" });
if (this.gatewayConf.gateway) {
this.gatewayOnline();
}
this.gatewayConf.on('gateway-online', () => this.gatewayOnline());
this.gatewayConf.on('gateway-offline', () => this.gatewayOffline());
this.setMessageListener();
}
gatewayOnline() {
this.status({ fill: "blue", shape: "dot", text: "online" });
}
gatewayOffline() {
this.status({ fill: "red", shape: "ring", text: "offline" });
}
setMessageListener() {
this.on('input', (msg) => {
if (this.gatewayConf.gateway) {
var payload = msg.payload;
// Input from gateway
if (payload.sid && payload.sid == this.gatewayConf.gateway.sid) {
if (payload.data.rgb) {
/*var decomposed = miDevicesUtils.computeColor(payload.data.rgb);
payload.data.brightness = decomposed.brightness;
payload.data.color = decomposed.color;*/
}
this.send(msg);
}
else {
msg.sid = this.gatewayConf.gateway.sid;
msg.gateway = this.gatewayConf.gateway;
this.send(msg);
}
}
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway`, Gateway);
};

View File

@@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const GatewayServer_1 = require("../../devices/gateway/GatewayServer");
exports.default = (RED) => {
class GatewayConfigurator {
constructor(props) {
RED.nodes.createNode(this, props);
let { sid, key, deviceList } = props;
this.sid = sid;
this.key = key;
this.deviceList = deviceList;
let server = GatewayServer_1.GatewayServer.getInstance();
if (this.sid) {
this.setGateway();
}
server.on('gateway-online', (sid) => {
if (sid === this.sid) {
this.setGateway();
this.emit('gateway-online');
}
});
server.on('gateway-offline', (sid) => {
if (sid === this.sid) {
this._gateway = null;
this.emit('gateway-offline');
}
});
}
setGateway() {
this._gateway = GatewayServer_1.GatewayServer.getInstance().getGateway(this.sid);
if (this._gateway) {
this._gateway.password = this.key;
this._gateway.on("subdevice-values-updated", (sidOrMessage) => {
let sid = sidOrMessage.sid || sidOrMessage;
let subdevice = this._gateway.getSubdevice(sid);
if (subdevice) {
(sidOrMessage.data ? Object.keys(sidOrMessage.data) : []).forEach((key) => {
subdevice[key] = sidOrMessage.data[key];
});
this.emit('subdevice-update', subdevice);
}
});
}
}
get gateway() {
return this._gateway;
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway configurator`, GatewayConfigurator, {
settings: {
miDevicesGatewayConfiguratorDiscoveredGateways: {
value: GatewayServer_1.GatewayServer.getInstance().gateways,
exportable: true
}
}
});
};

27
dist/nodes/gateway/GatewayIn.js vendored Normal file
View File

@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class GatewayIn {
constructor(props) {
RED.nodes.createNode(this, props);
this.gatewayConf = RED.nodes.getNode(props.gateway);
this.status({ fill: "red", shape: "ring", text: "offline" });
if (this.gatewayConf.gateway) {
this.gatewayOnline();
}
this.gatewayConf.on('gateway-online', () => this.gatewayOnline());
this.gatewayConf.on('gateway-offline', () => this.gatewayOffline());
}
gatewayOnline() {
this.status({ fill: "blue", shape: "dot", text: "online" });
this.gatewayConf.on('subdevice-update', (subdevice) => {
this.send({ payload: subdevice });
});
}
gatewayOffline() {
this.status({ fill: "red", shape: "ring", text: "offline" });
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway in`, GatewayIn);
};

35
dist/nodes/gateway/GatewayOut.js vendored Normal file
View File

@@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const GatewayServer_1 = require("../../devices/gateway/GatewayServer");
exports.default = (RED) => {
class GatewayOut {
constructor(props) {
RED.nodes.createNode(this, props);
this.setMessageListener();
}
setMessageListener() {
this.on("input", (msg) => {
if (msg.hasOwnProperty("payload") && msg.hasOwnProperty("gateway")) {
let gateway = GatewayServer_1.GatewayServer.getInstance().getGateway(msg.gateway.sid);
if (gateway) {
if (msg.payload.cmd) {
gateway.send(msg);
}
else if (msg.payload.action) {
switch (msg.payload.action) {
case 'setLight':
gateway.setLight(msg.payload.brightness, msg.payload.color);
break;
case 'playSound':
gateway.playSound(msg.payload.mid, msg.payload.volume);
break;
}
}
}
}
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway out`, GatewayOut);
};

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

295
dist/nodes/gateway/index.html vendored Normal file
View File

@@ -0,0 +1,295 @@
<script type="text/javascript">
RED.nodes.registerType('mi-devices-gateway configurator', {
category: 'config',
defaults: {
name: {value: ""},
sid: {value: ""},
key: { value: "" },
deviceList: {value:{}}
},
paletteLabel: "gateway configurator",
label: function () {
return this.name || "gateway configurator";
},
oneditprepare: function() {
var foundGateways = RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways;
Object.keys(foundGateways).forEach(function(sid) {
var gateway = foundGateways[sid];
$('#discovered-gateways').append('<option value="' + gateway.sid + '">' + gateway.sid + ' - ' + gateway.ip + '</option>');
});
var node = this;
var devicesConfig = {
"mi.weather": {label:"weather", icon:"icons/node-red-contrib-mi-devices/thermometer-icon.png"},
"mi.magnet": {label:"magnet", icon:"icons/node-red-contrib-mi-devices/door-icon.png"},
"mi.motion": {label:"motion", icon:"icons/node-red-contrib-mi-devices/motion-icon.png"},
"mi.switch": {label:"switch", icon:"icons/node-red-contrib-mi-devices/mi-switch.png"},
"mi.plug": {label:"plug zigbee", icon:"icons/node-red-contrib-mi-devices/outlet-icon.png"}
};
$("#node-config-input-subdevices").css('min-height','250px').css('min-width','450px').editableList({
addItem: function(container, i, device) {
var row = container;
$('<label/>',{for:"node-config-input-sid-"+i, style:"margin-left: 3px; width: 15px;vertical-align:middle"}).appendTo(row);
var sid = $('<input/>',{id:"node-config-input-sid-"+i,type:"text", placeholder:"SID", style:"width:auto;vertical-align:top"}).appendTo(row);
sid.typedInput({
default: 'mi.weather',
types: Object.keys(devicesConfig).map(function(type) {
var cleanType = devicesConfig[type];
cleanType.value = type;
return cleanType;
})
});
$('<label/>',{for:"node-config-input-desc-"+i, style:"margin-left: 7px; width: 20px;vertical-align:middle"}).html('<i class="fa fa-pencil-square-o"></i>').appendTo(row);
var desc = $('<input/>',{id:"node-config-input-desc-"+i, type:"text", placeholder:"name", style:"width:auto;vertical-align:top"}).appendTo(row);
sid.typedInput('value', device.sid);
sid.typedInput('type', device.internalModel);
desc.val(device.name);
container.parent().attr("data-sid", device.sid);
},
removeItem: function(device) {
$("#node-config-input-subdevices").find("[data-sid=" + device.sid + "]").remove();
},
sortable: false,
removable: true
});
$('#discovered-gateways').on('change', function() {
var sid = $('#discovered-gateways').val();
$('#input-subdevices > *').remove();
var gateway = sid && RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways[sid];
$("#node-config-input-sid").val(gateway && gateway.sid);
$("#node-config-input-key").val(gateway && gateway.key);
$("#node-config-input-subdevices").editableList('items').each(function(i, elt) {
$("#node-config-input-subdevices").editableList('removeItem', {sid: $(elt).find("#node-config-input-sid-"+i).val()});
});
var subdevices = gateway && Object.keys(gateway.subdevices).map(function(sid) { return gateway.subdevices[sid]; });
subdevices && subdevices.sort(function(a, b) { return a.internalModel > b.internalModel; }).forEach(function(device) {
if(!devicesConfig[device.internalModel] || !device.sid) {
return;
}
if(node.deviceList[device.sid]) {
device.name = node.deviceList[device.sid].name;
}
$("#node-config-input-subdevices").editableList('addItem', {
sid: device.sid,
internalModel: device.internalModel,
name: device.name
});
});
var listHeight = $("#node-config-input-subdevices").editableList('items').size() * 51 + 50;
$("#node-config-input-subdevices").editableList('height', listHeight);
});
Object.keys(this.deviceList || {}).forEach(function(sid) {
var device = node.deviceList[sid];
$("#node-config-input-subdevices").editableList('addItem', {
sid: sid,
internalModel: device.internalModel,
name: device.name
});
});
var listHeight = $("#node-config-input-subdevices").editableList('items').size() * 51 + 50;
$("#node-config-input-subdevices").editableList('height', listHeight);
},
oneditsave: function() {
var node = this;
var devices = $("#node-config-input-subdevices").editableList('items');
devices.each(function(i, elt) {
var deviceElement = $(elt);
var sid = deviceElement.find("#node-config-input-sid-"+i).val();
var desc = deviceElement.find("#node-config-input-desc-"+i).val();
var internalModel = deviceElement.find("#node-config-input-sid-"+i).typedInput('type');
node.deviceList[sid] = {internalModel: internalModel, name: desc};
});
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-gateway configurator">
<div class="form-row">
<label for="discovered-gateways"><i class="fa fa-search"></i> Found gateways</label>
<select id="discovered-gateways">
<option>- Select -</option>
</select>
</div>
<hr>
<h4>Gateway</h4>
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-config-input-sid"><i class="fa fa-barcode"></i> SID</label>
<input type="text" id="node-config-input-sid" placeholder="sid">
</div>
<div class="form-row">
<label for="node-input-gatewayKey"><i class="fa fa-key"></i> Key</label>
<input type="text" id="node-config-input-key" placeholder="Key">
</div>
<h4>Devices</h4>
<div class="form-row node-config-input-subdevices">
<ol id="node-config-input-subdevices"></ol>
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-gateway configurator">
<p>Gateway configuration for Xiaomi nodes.</p>
<h3>Details</h3>
<p>This configuration node is used by the Xiaomi device nodes. Here you can add
devices with their device-id (SID), type and a description.</p>
<p>At the moment the following devices are supported:
<lu>
<li>Humidity & Temperature sensor [sensor ht/]</li>
<li>Body motion sensor [motion]</li>
<li>Magnet contact sensor [contact]</li>
<li>Wall socket plug (zigbee) [plug]</li>
<li>Push button [switch]</li>
</lu>
</p>
<p>To be able to receive messages from the Xiaomi gateway, you need to set the gateway
in developer mode. Once in developer mode, the gateway sends JSON messages over the network as
UDP packages. On the internet their are a lot of guides on how to put the gateway in developer mode.</p>
<p>If you want to use the wall sockets, you need to set the key from the gateway. The key can be
retrieved via the Xiaomi Home App when in developer mode. Enter the key here and it is used
together with the token from the gateway's heartbeat message to recalculate the key to switch
the plug. If you do not specify a key, the plug-node can not be used.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-gateway', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"mi-devices-gateway configurator"},
name: {value: ""}
},
inputs: 1,
outputs: 1,
outputLabels: ["Gateway"],
paletteLabel: "gateway",
icon: "mijia.png",
label: function () {
return this.name || "gateway";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-gateway">
<div class="form-row">
<label for="node-input-gateway"><i class="fa fa-wrench"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-gateway">
<p>The gateway itself.</p>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Devices output
<dl class="message-properties">
<dt>gateway <span class="property-type">mi-devices-gateway configurator</span></dt>
<dd>The gateway.</dd>
</dl>
<dl class="message-properties">
<dt>payload <span class="property-type">json</span></dt>
<dd>Data from gateway, with computed data.</dd>
</dl>
</li>
</ol>
</script>
<script type="text/x-red" data-template-name="mi-devices-gateway in">
<div class="form-row">
<label for="node-input-gateway"><i class="fa fa-wrench"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-gateway in">
<p>A Xiaomi Gateway input node, that produces a <code>msg.payload</code> containing a
string with the gateway message content.
</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-gateway in',{
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""},
gateway: {value:"", type:"mi-devices-gateway configurator"}
},
inputs:0,
outputs:1,
paletteLabel: "gateway in",
icon: "mijia-io.png",
label: function() {
return this.name||"gateway in";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
function changeGateway() {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
if(!this.name) {
$("#node-input-name").val(configNode.name);
}
$('#node-input-ip').val(configNode.ip);
}
}
}
$("#node-input-gateway").change(function () {
changeGateway();
});
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-gateway out">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-gateway out">
<p>This node sends <code>msg.payload</code> to <code>msg.gateway</code> Xiaomi Gateway.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-gateway out',{
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""}
},
inputs:1,
outputs:0,
paletteLabel: "gateway out",
icon: "mijia-io.png",
align: "right",
label: function() {
return this.name||"gateway out";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
</script>

13
dist/nodes/gateway/index.js vendored Normal file
View File

@@ -0,0 +1,13 @@
"use strict";
const GatewayServer_1 = require("../../devices/gateway/GatewayServer");
const GatewayConfigurator_1 = require("./GatewayConfigurator");
const Gateway_1 = require("./Gateway");
const GatewayIn_1 = require("./GatewayIn");
const GatewayOut_1 = require("./GatewayOut");
module.exports = (RED) => {
GatewayServer_1.GatewayServer.getInstance().discover();
GatewayConfigurator_1.default(RED);
Gateway_1.default(RED);
GatewayIn_1.default(RED);
GatewayOut_1.default(RED);
};

View File

@@ -1,5 +1,5 @@
<script type="text/javascript">
RED.nodes.registerType('xiaomi-plug-wifi', {
RED.nodes.registerType('mi-devices-plug-wifi', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
@@ -15,12 +15,12 @@
paletteLabel: "plug (wifi)",
icon: "outlet-wifi-icon.png",
label: function () {
return this.name || "xiaomi-plug-wifi";
return this.name || "plug-wifi";
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-plug-wifi">
<script type="text/x-red" data-template-name="mi-devices-plug-wifi">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
@@ -31,7 +31,7 @@
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-plug-wifi">
<script type="text/x-red" data-help-name="mi-devices-plug-wifi">
<p>The Xiaomi plug (wifi) node</p>
<p>This is the wiFi plug (socket). To control the Wifi-Plug, extensive use is made of the miio library created by <a href="https://github.com/aholstenson/miio">Andreas Holstenson</a>. Make sure to check his page for compatible devices.</p>

139
dist/nodes/plug-wifi/index.js vendored Normal file
View File

@@ -0,0 +1,139 @@
"use strict";
const constants_1 = require("../constants");
const miio = require("miio");
module.exports = (RED) => {
var connectionState = "timeout";
var retryTimer;
var delayedStatusMsgTimer;
function XiaomiPlugWifiNode(config) {
RED.nodes.createNode(this, config);
this.ip = config.ip;
this.plug = null;
this.status({ fill: "yellow", shape: "dot", text: "connecting" });
miio.device({ address: this.ip })
.then((plug) => {
this.plug = plug;
this.status({ fill: "green", shape: "dot", text: "connected" });
connectionState = "connected";
delayedStatusMsgUpdate();
this.plug.on('propertyChanged', (e) => {
if (e.property === "power") {
if (e.value['0']) {
setState("on");
}
else {
setState("off");
}
}
});
watchdog();
})
.catch((error) => {
connectionState = "reconnecting";
watchdog();
});
this.on('input', (msg) => {
var payload = msg.payload;
if (connectionState === "connected") {
if (payload == 'on') {
this.plug.setPower(true);
}
if (payload == 'off') {
this.plug.setPower(false);
}
}
});
this.on('close', (done) => {
if (retryTimer) {
clearTimeout(retryTimer);
}
if (delayedStatusMsgTimer) {
clearTimeout(delayedStatusMsgTimer);
}
if (this.plug) {
this.plug.destroy();
}
done();
});
var setState = (state) => {
if (this.plug) {
let status = {
payload: {
id: this.plug.id,
type: this.plug.type,
model: this.plug.model,
capabilities: this.plug.capabilities,
address: this.plug.address,
port: this.plug.port,
power: this.plug.power(),
state: state
}
};
this.send(status);
}
};
var delayedStatusMsgUpdate = () => {
delayedStatusMsgTimer = setTimeout(() => {
if (this.plug.power()['0']) {
setState("on");
}
else {
setState("off");
}
}, 1500);
};
var discoverDevice = () => {
miio.device({ address: this.ip })
.then((plug) => {
if (this.plug == null) {
this.plug = plug;
this.plug.on('propertyChanged', (e) => {
if (e.property === "power") {
if (e.value['0']) {
setState("on");
}
else {
setState("off");
}
}
});
}
if (connectionState === "reconnecting") {
this.status({ fill: "green", shape: "dot", text: "connected" });
connectionState = "connected";
delayedStatusMsgUpdate();
}
})
.catch((error) => {
connectionState = "reconnecting";
if (this.plug) {
this.plug.destroy();
this.plug = null;
}
});
};
var watchdog = () => {
var node = this;
function retryTimer() {
discoverDevice();
if (connectionState === "reconnecting") {
node.status({ fill: "red", shape: "dot", text: "reconnecting" });
}
setTimeout(retryTimer, 30000);
}
setTimeout(retryTimer, 30000);
};
}
process.on('unhandledRejection', function (reason, p) {
// console.log("Possibly Unhandled Rejection at: Promise ", p, " reason: ", reason);
var message = reason + "";
if (message.indexOf("Call to device timed out") >= 0) {
if (this.plug) {
console.log("Issue with miio package; discard plug and reconnect.");
this.plug.destroy();
this.plug = null;
}
}
});
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-wifi-plug`, XiaomiPlugWifiNode);
};

View File

@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const YeelightServer_1 = require("../../devices/yeelight/YeelightServer");
exports.default = (RED) => {
class YeelightConfigurator {
constructor(props) {
RED.nodes.createNode(this, props);
let { sid } = props;
this.sid = parseInt(sid);
if (this.sid) {
this.setBulb();
}
let server = YeelightServer_1.YeelightServer.getInstance();
server.on('yeelight-online', (sid) => {
if (sid === this.sid) {
this.setBulb();
this.emit('bulb-online');
}
});
server.on('yeelight-offline', (sid) => {
if (sid === this.sid) {
this._bulb = null;
this.emit('bulb-offline');
}
});
}
setBulb() {
this._bulb = YeelightServer_1.YeelightServer.getInstance().getBulb(this.sid);
}
get bulb() {
return this._bulb;
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-yeelight configurator`, YeelightConfigurator, {
settings: {
miDevicesYeelightConfiguratorDiscoveredBulbs: {
value: YeelightServer_1.YeelightServer.getInstance().bulbs,
exportable: true
}
}
});
};

64
dist/nodes/yeelight/YeelightOut.js vendored Normal file
View File

@@ -0,0 +1,64 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class YeelightOut {
constructor(props) {
RED.nodes.createNode(this, props);
this.yeelightConf = RED.nodes.getNode(props.yeelight);
this.status({ fill: "red", shape: "ring", text: "offline" });
if (this.yeelightConf.bulb) {
this.yeelightOnline();
}
this.yeelightConf.on('bulb-online', () => this.yeelightOnline());
this.yeelightConf.on('bulb-offline', () => this.yeelightOffline());
this.setListener();
}
yeelightOnline() {
this.status({ fill: "blue", shape: "dot", text: "online" });
}
yeelightOffline() {
this.status({ fill: "red", shape: "ring", text: "offline" });
}
setListener() {
this.on("input", (msg) => {
let bulb = this.yeelightConf.bulb;
if (msg.hasOwnProperty("payload") && bulb) {
switch (msg.payload.action) {
case 'turn_on':
bulb.turnOn();
break;
case 'turn_off':
bulb.turnOff();
break;
case 'toggle':
bulb.toggle();
break;
case 'setLight':
if (msg.payload.color !== undefined) {
let rgb = msg.payload.color.blue | (msg.payload.color.green << 8) | (msg.payload.color.red << 16);
let hex = '#' + (0x1000000 + rgb).toString(16).slice(1);
bulb.setRGB(hex);
}
(msg.payload.brightness !== undefined) && bulb.setBrightness(Math.max(1, msg.payload.brightness));
break;
}
}
});
/*(<any> this).on('input', (msg) => {
if (this.yeelightConf.bulb) {
if(msg.payload.color !== undefined) {
// TODO: revoir la couleur
this.yeelightConf.bulb.setRGB(msg.payload.color);
}
if(msg.payload.brightness !== undefined) {
this.yeelightConf.bulb.setBrightness(msg.payload.brightness);
}
}
});*/
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-yeelight out`, YeelightOut);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

160
dist/nodes/yeelight/index.html vendored Normal file
View File

@@ -0,0 +1,160 @@
<script type="text/javascript">
RED.nodes.registerType('mi-devices-yeelight configurator', {
category: 'config',
defaults: {
name: {value: ""},
sid: {value: ""}
},
label: function () {
return this.name || "yeelight conf";
},
oneditprepare: function() {
var foundBulbs = RED.settings.miDevicesYeelightConfiguratorDiscoveredBulbs;
Object.keys(foundBulbs).forEach(function(sid) {
var bulb = foundBulbs[sid];
$('#discovered-bulbs').append('<option value="' + bulb.sid + '">' + (bulb.name || bulb.sid) + ' - ' + bulb.model + ' - ' + bulb.ip + '</option>');
});
$('#discovered-bulbs').on('change', function() {
var sid = $('#discovered-bulbs').val();
var bulb = foundBulbs[sid];
$("#node-config-input-name").val(bulb && bulb.name);
$("#node-config-input-sid").val(bulb && bulb.sid);
});
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-yeelight configurator">
<div class="form-row">
<label for="discovered-bulbs"><i class="fa fa-search"></i> Found bulbs</label>
<select id="discovered-bulbs">
<option>- Select -</option>
</select>
</div>
<hr>
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-config-input-sid"><i class="fa fa-barcode"></i> SID</label>
<input type="text" id="node-config-input-sid" placeholder="sid">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-yeelight configurator">
<p>Xiaomi Yeelight configuration node.</p>
<h3>Details</h3>
<p>This configuration node is used by the Yeelight nodes. Here you can add
devices with their device-id (SID), type and a description.</p>
<p>At the moment the following devices are supported:
<lu>
<li>Humidity & Temperature sensor [sensor ht/]</li>
<li>Body motion sensor [motion]</li>
<li>Magnet contact sensor [contact]</li>
<li>Wall socket plug (zigbee) [plug]</li>
<li>Push button [switch]</li>
</lu>
</p>
<p>To be able to receive messages from the Xiaomi gateway, you need to set the gateway
in developer mode. Once in developer mode, the gateway sends JSON messages over the network as
UDP packages. On the internet their are a lot of guides on how to put the gateway in developer mode.</p>
<p>If you want to use the wall sockets, you need to set the key from the gateway. The key can be
retrieved via the Xiaomi Home App when in developer mode. Enter the key here and it is used
together with the token from the gateway's heartbeat message to recalculate the key to switch
the plug. If you do not specify a key, the plug-node can not be used.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-yeelight out', {
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value: ""},
yeelight: {value:"", type:"mi-devices-yeelight configurator"}
},
inputs: 1,
outputs: 0,
paletteLabel: "yeelight out",
icon: "mi-yeelight.png",
align: "right",
label: function () {
return this.name || "yeelight out";
},
oneditprepare: function() {
function changeGateway() {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
if(!this.name) {
$("#node-input-name").val(configNode.name);
}
$('#node-input-ip').val(configNode.ip);
}
}
}
$("#node-input-gateway").change(function () {
changeGateway();
});
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-yeelight out">
<div class="form-row">
<label for="node-input-yeelight"><i class="fa fa-wrench"></i> Yeelight</label>
<input type="text" id="node-input-yeelight" placeholder="yeelight">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-yeelight out">
<p>The Xiaomi Yeelight node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
<hr>
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample message:</p>
<p><pre>{
cmd: "report"
model: "switch"
sid: "158d000128b124"
short_id: 56773
data: {
status: "click",
batteryLevel: 23
}
}</pre></p>
</script>

9
dist/nodes/yeelight/index.js vendored Normal file
View File

@@ -0,0 +1,9 @@
"use strict";
const YeelightServer_1 = require("../../devices/yeelight/YeelightServer");
const YeelightConfigurator_1 = require("./YeelightConfigurator");
const YeelightOut_1 = require("./YeelightOut");
module.exports = (RED) => {
YeelightServer_1.YeelightServer.getInstance().discover();
YeelightConfigurator_1.default(RED);
YeelightOut_1.default(RED);
};

23
dist/utils/Color.js vendored Normal file
View File

@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Color {
static toValue(red, green, blue, brightness) {
return (brightness !== undefined ? 256 * 256 * 256 * brightness : 0) + (256 * 256 * red) + (256 * green) + blue;
}
static fromValue(rgb) {
var blue = rgb % 256;
rgb = Math.max(rgb - blue, 0);
var green = rgb % (256 * 256);
rgb = Math.max(rgb - green, 0);
green /= 256;
var red = rgb % (256 * 256 * 256);
rgb = Math.max(rgb - red, 0);
red /= 256 * 256;
var brightness = rgb / (256 * 256 * 256);
return {
brightness: brightness,
color: { red: red, green: green, blue: blue }
};
}
}
exports.Color = Color;

6
dist/utils/index.js vendored Normal file
View File

@@ -0,0 +1,6 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./Color"));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
icons/actions/mi-bulb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

BIN
icons/actions/mi-click.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
icons/actions/mi-list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

BIN
icons/actions/mi-mute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
icons/actions/mi-off.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
icons/actions/mi-on.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/actions/mi-read.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

BIN
icons/actions/mi-sound.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
icons/actions/mi-toggle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
icons/gateway/mijia-io.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
icons/gateway/mijia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,185 +0,0 @@
const miDevicesUtils = require('../src/utils');
module.exports = (RED) => {
/*********************************************
Read data from Gateway
*********************************************/
function XiaomiActionRead(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
if(msg.sid) {
msg.payload = { cmd: "read", sid: msg.sid };
this.send(msg);
}
});
}
RED.nodes.registerType("xiaomi-actions read", XiaomiActionRead);
/*********************************************
Get registred ids of devices on gateway
*********************************************/
function XiaomiActionGetIdList(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
msg.payload = { cmd: "get_id_list" };
node.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions get_id_list", XiaomiActionGetIdList);
/*********************************************
Virtual single click on a button
*********************************************/
function XiaomiActionSingleClick(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
msg.payload = {
cmd: "write",
data: { status: "click", sid: msg.sid }
};
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions click", XiaomiActionSingleClick);
/*********************************************
Virtual Double click on a button
*********************************************/
function XiaomiActionDoubleClick(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
msg.payload = {
cmd: "write",
data: { status: "double_click", sid: msg.sid }
};
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions double_click", XiaomiActionDoubleClick);
/*********************************************
Set the gateway light
*********************************************/
function XiaomiActionGatewayLight(config) {
RED.nodes.createNode(this, config);
this.color = config.color;
this.brightness = config.brightness;
this.on('input', (msg) => {
let color = msg.color || this.color;
let brightness = msg.brightness || this.brightness;
if(msg.sid) {
let rgb = miDevicesUtils.computeColorValue(color.red, color.green, color.blue, brightness);
msg.payload = {
cmd: "write",
data: { rgb: rgb, sid: msg.sid }
};
}
else {
msg.payload = {
color: miDevicesUtils.computeColorValue(color.red, color.green, color.blue),
brightness: brightness
};
}
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions gateway_light", XiaomiActionGatewayLight);
/*********************************************
Play a sound on the gateway
*********************************************/
function XiaomiActionGatewaySound(config) {
RED.nodes.createNode(this, config);
this.mid = config.mid;
this.volume = config.volume;
this.on('input', (msg) => {
msg.payload = {
cmd: "write",
data: {
mid: parseInt(msg.mid || this.mid),
volume: parseInt(msg.volume || this.volume),
sid: msg.sid
}
};
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions gateway_sound", XiaomiActionGatewaySound);
/*********************************************
Stop playing a sound on the gateway
*********************************************/
function XiaomiActionGatewayStopSound(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
msg.payload = {
cmd: "write",
data: { mid: 1000, sid: msg.sid }
};
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions gateway_stop_sound", XiaomiActionGatewayStopSound);
/*********************************************
Turn device on
*********************************************/
function XiaomiActionPowerOn(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
if(msg.sid){
msg.payload = {
cmd: "write",
data: { status: "on", sid: msg.sid }
};
}
else {
msg.payload = "on";
}
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions on", XiaomiActionPowerOn);
/*********************************************
Turn device off
*********************************************/
function XiaomiActionPowerOff(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
if(msg.sid){
msg.payload = {
cmd: "write",
data: { status: "off", sid: msg.sid }
};
}
else {
msg.payload = "off";
}
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions off", XiaomiActionPowerOff);
/*********************************************
Toggle device
*********************************************/
function XiaomiActionToggle(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
msg.payload = "toggle";
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions toggle", XiaomiActionToggle);
}

View File

@@ -1,50 +0,0 @@
module.exports = (RED) => {
function getOnlyModelsValue(input) {
var cleanOnlyModels = [];
input.forEach((value) => {
cleanOnlyModels = cleanOnlyModels.concat(value.split(','));
});
return cleanOnlyModels;
}
function XiaomiAllNode(config) {
RED.nodes.createNode(this, config);
this.gateway = RED.nodes.getNode(config.gateway);
this.onlyModels = getOnlyModelsValue(config.onlyModels || []);
this.excludedSids = config.excludedSids;
this.isDeviceValid = (device) => {
if((!this.onlyModels || this.onlyModels.length == 0) && (!this.excludedSids || this.excludedSids.length == 0)) {
return true;
}
// Is excluded
if((this.excludedSids && this.excludedSids.length != 0) && this.excludedSids.indexOf(device.sid) >= 0) {
return false;
}
if((this.onlyModels && this.onlyModels.length != 0) && this.onlyModels.indexOf(device.model) >= 0) {
return true;
}
return false;
}
if (this.gateway) {
this.on('input', (msg) => {
// Filter input
if(msg.payload && msg.payload.model && msg.payload.sid) {
if(!this.isDeviceValid(msg.payload)) {
msg = null;
}
}
// Prepare for request
else {
msg.payload = this.gateway.deviceList.filter((device) => this.isDeviceValid(device));
}
this.send(msg);
});
}
}
RED.nodes.registerType("xiaomi-all", XiaomiAllNode);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,131 +0,0 @@
<script type="text/javascript">
RED.nodes.registerType('xiaomi-configurator', {
category: 'config',
defaults: {
name: {value: ""},
ip: {value: "", required: true},
sid: {value: ""},
deviceList: {value:[{ sid:"", desc:"", model:"plug"}]},
key: {value: ""}
},
label: function () {
return this.name || "xiaomi-configurator";
},
oneditprepare: function() {
var node = this;
var tw_sensor_ht = {value:"sensor_ht", label:"sensor ht", icon:"icons/node-red-contrib-mi-devices/thermometer-tw-icon.png"};
var tw_magnet = {value:"magnet", label:"contact", icon:"icons/node-red-contrib-mi-devices/door-tw-icon.png"};
var tw_motion = {value:"motion", label:"motion", icon:"icons/node-red-contrib-mi-devices/motion-tw-icon.png"};
var tw_plug = {value:"plug", label:"plug", icon:"icons/node-red-contrib-mi-devices/outlet-tw-icon.png"};
var tw_switch = {value:"switch", label:"switch", icon:"icons/node-red-contrib-mi-devices/mi-tw-switch.png"};
$("#node-config-input-devices").css('min-height','250px').css('min-width','450px').editableList({
addItem: function(container, i, device) {
if (!device.hasOwnProperty('model')) {
device.model = 'sensor_ht';
}
var row = $('<div/>').appendTo(container);
$('<label/>',{for:"node-config-input-sid-"+i, style:"margin-left: 3px; width: 15px;vertical-align:middle"}).appendTo(row);
var sid = $('<input/>',{id:"node-config-input-sid-"+i,type:"text", placeholder:"SID", style:"width:auto;vertical-align:top"}).appendTo(row);
sid.typedInput({
default: 'sensor_ht',
types: [tw_sensor_ht, tw_magnet, tw_motion, tw_plug, tw_switch]
});
$('<label/>',{for:"node-config-input-desc-"+i, style:"margin-left: 7px; width: 20px;vertical-align:middle"}).html('<i class="fa fa-pencil-square-o"></i>').appendTo(row);
var desc = $('<input/>',{id:"node-config-input-desc-"+i, type:"text", placeholder:"description", style:"width:auto;vertical-align:top"}).appendTo(row);
sid.typedInput('value', device.sid);
sid.typedInput('type', device.model);
desc.val(device.desc);
},
resize: function() {
},
removeItem: function(opt) {
},
sortItems: function(rules) {
},
sortable: true,
removable: true
});
for (var i=0;i<this.deviceList.length;i++) {
var device = this.deviceList[i];
$("#node-config-input-devices").editableList('addItem', device);
}
var listHeight = $("#node-config-input-devices").editableList('items').size() * 51 + 50;
$("#node-config-input-devices").editableList('height', listHeight);
},
oneditsave: function() {
var devices = $("#node-config-input-devices").editableList('items');
var node = this;
RED.nodes.eachNode(function(tmpNode) {
if(tmpNode.type.indexOf("xiaomi-gateway") === 0 && tmpNode.gateway == node.id) {
tmpNode.ip = $("#node-config-input-ip").val();
tmpNode.changed = true;
}
});
var devicesArray = [];
devices.each(function(i) {
var deviceElement = $(this);
var sid = deviceElement.find("#node-config-input-sid-"+i).val();
var desc = deviceElement.find("#node-config-input-desc-"+i).val();
var model = deviceElement.find("#node-config-input-sid-"+i).typedInput('type');
var d = {};
d['sid']=sid;
d['desc']=desc;
d['model']=model;
devicesArray.push(d);
});
node.deviceList = devicesArray;
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-configurator">
<div class="form-row">
<label for="node-config-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-config-input-ip"><i class="icon-tag"></i> IP address (v4 or v6)</label>
<input type="text" id="node-config-input-ip" placeholder="IP">
</div>
<div class="form-row">
<label for="node-config-input-sid"><i class="icon-tag"></i> SID (optional)</label>
<input type="text" id="node-config-input-sid" placeholder="sid">
</div>
<div class="form-row">
<label for="node-config-input-key"><i class="fa fa-ticket"></i> Key/Password</label>
<input type="text" id="node-config-input-key" placeholder="Key">
</div>
<div class="form-row node-config-input-devices">
<ol id="node-config-input-devices"></ol>
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-configurator">
<p>Device configuration for Xiaomi nodes.</p>
<h3>Details</h3>
<p>This configuration node is used by the Xiaomi device nodes. Here you can add
devices with their device-id (SID), type and a description.</p>
<p>At the moment the following devices are supported:
<lu>
<li>Humidity & Temperature sensor [sensor ht/]</li>
<li>Body motion sensor [motion]</li>
<li>Magnet contact sensor [contact]</li>
<li>Wall socket plug (zigbee) [plug]</li>
<li>Push button [switch]</li>
</lu>
</p>
<p>To be able to receive messages from the Xiaomi gateway, you need to set the gateway
in developer mode. Once in developer mode, the gateway sends JSON messages over the network as
UDP packages. On the internet their are a lot of guides on how to put the gateway in developer mode.</p>
<p>If you want to use the wall sockets, you need to set the key from the gateway. The key can be
retrieved via the Xiaomi Home App when in developer mode. Enter the key here and it is used
together with the token from the gateway's heartbeat message to recalculate the key to switch
the plug. If you do not specify a key, the plug-node can not be used.</p>
</script>

View File

@@ -1,14 +0,0 @@
module.exports = (RED) => {
function XiaomiConfiguratorNode(n) {
RED.nodes.createNode(this, n);
this.name = n.name;
this.deviceList = n.deviceList || [];
this.key = n.key;
this.ip = n.ip;
this.sid = this.sid || n.sid;
var node = this;
}
RED.nodes.registerType("xiaomi-configurator", XiaomiConfiguratorNode);
}

View File

@@ -1,172 +0,0 @@
<!-- The Input Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-gateway', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"xiaomi-configurator"},
name: {value: ""}
},
inputs: 1,
outputs: 1,
outputLabels: ["Gateway"],
paletteLabel: "gateway",
icon: "mijia.png",
label: function () {
return this.name || "xiaomi-gateway";
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-gateway">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-gateway">
<p>The gateway itself.</p>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Devices output
<dl class="message-properties">
<dt>gateway <span class="property-type">xiaomi-configurator</span></dt>
<dd>The gateway.</dd>
</dl>
<dl class="message-properties">
<dt>payload <span class="property-type">json</span></dt>
<dd>Data from gateway, with computed data.</dd>
</dl>
</li>
</ol>
</script>
<!-- The Input Node -->
<script type="text/x-red" data-template-name="xiaomi-gateway in">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-ip"><i class="icon-tag"></i> IP</label>
<input type="text" id="node-input-ip" placeholder="IP" readonly>
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-gateway in">
<p>A Xiaomi Gateway input node, that produces a <code>msg.payload</code> containing a
string with the gateway message content.
</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-gateway in',{
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""},
gateway: {value:"", type:"xiaomi-configurator"},
ip: {value:""}
},
inputs:0,
outputs:1,
paletteLabel: "gateway in",
icon: "mijia-io.png",
label: function() {
return this.name||"xiaomi-gateway";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
function changeGateway() {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
if(!this.name) {
$("#node-input-name").val(configNode.name);
}
$('#node-input-ip').val(configNode.ip);
}
}
}
$("#node-input-gateway").change(function () {
changeGateway();
});
}
});
</script>
<!-- The Output Node -->
<script type="text/x-red" data-template-name="xiaomi-gateway out">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-ip"><i class="icon-tag"></i> IP</label>
<input type="text" id="node-input-ip" placeholder="IP" readonly>
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-gateway out">
<p>This node sends <code>msg.payload</code> to the configured Xiaomi Gateway.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-gateway out',{
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""},
gateway: {value:"", type:"xiaomi-configurator"},
ip: {value:""}
},
inputs:1,
outputs:0,
paletteLabel: "gateway out",
icon: "mijia-io.png",
align: "right",
label: function() {
return this.name||"xiaomi-gateway";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
function changeGateway() {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
if(!this.name) {
$("#node-input-name").val(configNode.name);
}
$('#node-input-ip').val(configNode.ip);
}
}
}
$("#node-input-gateway").change(function () {
changeGateway();
});
}
});
</script>

View File

@@ -1,229 +0,0 @@
const dgram = require('dgram'); // Given by udp node
const miDevicesUtils = require('../src/utils');
// UDP node copy/paste...
module.exports = (RED) => {
var udpInputPortsInUse = {};
function XiaomiGatewayNode(config) {
RED.nodes.createNode(this, config);
this.gateway = RED.nodes.getNode(config.gateway);
this.status({fill:"red", shape:"ring", text: "offline"});
if (this.gateway) {
this.on('input', (msg) => {
var payload = msg.payload;
// Input from gateway
if(payload.sid) {
if (payload.sid == this.gateway.sid) {
if(payload.data.rgb) {
var decomposed = miDevicesUtils.computeColor(payload.data.rgb);
payload.data.brightness = decomposed.brightness;
payload.data.color = decomposed.color;
}
this.send([msg]);
}
}
// Prepare for request
else {
msg.gateway = this.gateway;
msg.sid = this.gateway.sid;
this.send(msg);
}
});
}
}
RED.nodes.registerType("xiaomi-gateway", XiaomiGatewayNode);
// The Input Node
function GatewayIn(n) {
RED.nodes.createNode(this,n);
this.gatewayNodeId = n.gateway;
this.gateway = RED.nodes.getNode(n.gateway);
this.group = "224.0.0.50";
this.port = 9898;
this.iface = null;
this.addr = n.ip;
this.ipv = this.ip && this.ip.indexOf(":") >= 0 ? "udp6" : "udp4";
this.status({fill:"red", shape:"ring", text: "offline"});
var opts = {type:this.ipv, reuseAddr:true};
if (process.version.indexOf("v0.10") === 0) { opts = this.ipv; }
var server;
if (!udpInputPortsInUse.hasOwnProperty(this.port)) {
server = dgram.createSocket(opts); // default to udp4
udpInputPortsInUse[this.port] = server;
}
else {
this.warn(RED._("udp.errors.alreadyused",this.port));
server = udpInputPortsInUse[this.port]; // re-use existing
}
if (process.version.indexOf("v0.10") === 0) { opts = this.ipv; }
server.on("error", (err) => {
if ((err.code == "EACCES") && (this.port < 1024)) {
this.error(RED._("udp.errors.access-error"));
} else {
this.error(RED._("udp.errors.error",{error:err.code}));
}
server.close();
});
server.on('message', (message, remote) => {
var msg;
if(remote.address == this.addr) {
var msg = message.toString('utf8');
var jsonMsg = JSON.parse(msg);
if(jsonMsg.data) {
jsonMsg.data = JSON.parse(jsonMsg.data) || jsonMsg.data;
if(jsonMsg.data.voltage) {
jsonMsg.data.batteryLevel = miDevicesUtils.computeBatteryLevel(jsonMsg.data.voltage);
}
}
msg = { payload: jsonMsg };
if(this.gateway && jsonMsg.data.ip && jsonMsg.data.ip === this.gateway.ip) {
if(jsonMsg.token) {
this.gateway.lastToken = jsonMsg.token;
if(!this.gateway.sid) {
this.gateway.sid = jsonMsg.sid;
}
}
RED.nodes.eachNode((tmpNode) => {
if(tmpNode.type.indexOf("xiaomi-gateway") === 0 && tmpNode.gateway == this.gatewayNodeId) {
let tmpNodeInst = RED.nodes.getNode(tmpNode.id);
if(tmpNode.type === "xiaomi-gateway out" && !this.gateway.lastToken) {
tmpNodeInst.status({fill:"yellow", shape:"ring", text: "waiting input"});
}
tmpNodeInst.status({fill:"blue", shape:"dot", text: "online"});
}
});
}
this.send(msg);
}
});
server.on('listening', () => {
var address = server.address();
this.log(RED._("udp.status.listener-at",{host:address.address,port:address.port}));
server.setBroadcast(true);
try {
server.setMulticastTTL(128);
server.addMembership(this.group,this.iface);
this.log(RED._("udp.status.mc-group",{group:this.group}));
} catch (e) {
if (e.errno == "EINVAL") {
this.error(RED._("udp.errors.bad-mcaddress"));
} else if (e.errno == "ENODEV") {
this.error(RED._("udp.errors.interface"));
} else {
this.error(RED._("udp.errors.error",{error:e.errno}));
}
}
});
this.on("close", () => {
if (udpInputPortsInUse.hasOwnProperty(this.port)) {
delete udpInputPortsInUse[this.port];
}
try {
server.close();
this.log(RED._("udp.status.listener-stopped"));
} catch (err) {
//this.error(err);
}
});
try { server.bind(this.port, this.iface); }
catch(e) { } // Don't worry if already bound
}
RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-ports.read'), (req,res) => {
res.json(Object.keys(udpInputPortsInUse));
});
RED.nodes.registerType("xiaomi-gateway in",GatewayIn);
// The Output Node
function GatewayOut(n) {
RED.nodes.createNode(this,n);
this.port = 9898;
this.outport = 9898;
this.iface = null;
this.addr = n.ip;
this.ipv = this.ip && this.ip.indexOf(":") >= 0 ? "udp6" : "udp4";
this.multicast = false;
this.gatewayNodeId = n.gateway;
this.gateway = RED.nodes.getNode(n.gateway);
this.status({fill:"red", shape:"ring", text: "offline"});
var opts = {type:this.ipv, reuseAddr:true};
if (process.version.indexOf("v0.10") === 0) { opts = this.ipv; }
var sock;
if (udpInputPortsInUse[this.outport]) {
sock = udpInputPortsInUse[this.outport];
}
else {
sock = dgram.createSocket(opts); // default to udp4
sock.on("error", (err) => {
// Any async error will also get reported in the sock.send call.
// This handler is needed to ensure the error marked as handled to
// prevent it going to the global error handler and shutting node-red
// down.
});
udpInputPortsInUse[this.outport] = sock;
}
if (!udpInputPortsInUse[this.outport]) {
sock.bind(this.outport);
this.log(RED._("udp.status.ready",{outport:this.outport,host:this.addr,port:this.port}));
} else {
this.log(RED._("udp.status.ready-nolocal",{host:this.addr,port:this.port}));
}
this.on("input", (msg) => {
if (msg.hasOwnProperty("payload")) {
var add = this.addr || msg.ip || "";
var por = this.port || msg.port || 0;
if (add === "") {
this.warn(RED._("udp.errors.ip-notset"));
} else if (por === 0) {
this.warn(RED._("udp.errors.port-notset"));
} else if (isNaN(por) || (por < 1) || (por > 65535)) {
this.warn(RED._("udp.errors.port-invalid"));
} else {
if(msg.payload.cmd === "write" && !msg.payload.data.key && this.gateway && this.gateway.sid && this.gateway.key && this.gateway.lastToken) {
msg.payload.data.key = miDevicesUtils.getGatewayKey(this.gateway.key, this.gateway.lastToken);
}
var message = Buffer.from(JSON.stringify(msg.payload));
sock.send(message, 0, message.length, por, add, (err, bytes) => {
if (err) {
this.error("udp : "+err,msg);
}
message = null;
});
}
}
});
this.on("close", () => {
if (udpInputPortsInUse.hasOwnProperty(this.outport)) {
delete udpInputPortsInUse[this.outport];
}
try {
sock.close();
this.log(RED._("udp.status.output-stopped"));
} catch (err) {
//this.error(err);
}
});
}
RED.nodes.registerType("xiaomi-gateway out", GatewayOut);
}

View File

@@ -1,122 +0,0 @@
<script type="text/javascript">
RED.nodes.registerType('xiaomi-ht', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"xiaomi-configurator"},
name: {value: ""},
sid: {value: "", required: true}
},
inputs: 1,
outputs: 1,
paletteLabel: "sensor HT",
icon: "thermometer-icon.png",
label: function () {
return this.name || "xiaomi-ht";
},
oneditprepare: function() {
var node = this;
if(node.sid) {
$('#node-input-sid').val(node.sid);
}
function changeGateway(model) {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
$('#node-input-sid').empty();
for (key in configNode.deviceList) {
var device = configNode.deviceList[key];
if (device.model === model) {
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + '</option>');
}
}
if(node.sid) {
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
}
}
}
}
$("#node-input-sid").change(function () {
if(!this.name) {
$("#node-input-name").val($('#node-input-sid option:selected').text());
}
});
$("#node-input-gateway").change(function () {
changeGateway("sensor_ht");
});
},
oneditsave: function() {
var node = this;
node.sid = $("#node-input-sid").val();
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-ht">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-ht">
<p>The Xiaomi Humidity & Temperature sensor node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
<hr>
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample payload from Aqara (which brings pressure):</p>
<p><pre>{
cmd: "read_ack"
model: "weather.v1"
sid: "158d00010b7f1b"
short_id: 8451
data: {
voltage:3005,
temperature:23.25,
humidity:56.99,
pressure:981.26,
batteryLevel: 34
}
}</pre>
Where <code>humidy</code> is in percents, <code>pressure</code> in hPa, <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
</script>

View File

@@ -1,41 +0,0 @@
const miDevicesUtils = require('../src/utils');
module.exports = (RED) => {
// sensor_ht, weather.v1
function XiaomiHtNode(config) {
RED.nodes.createNode(this, config);
this.gateway = RED.nodes.getNode(config.gateway);
this.sid = config.sid;
this.status({fill:"grey", shape:"ring", text:"battery - na"});
if (this.gateway) {
this.on('input', (msg) => {
let payload = msg.payload;
// Input from gateway
if (payload.sid) {
if (payload.sid == this.sid) {
miDevicesUtils.setStatus(this, payload.data);
["temperature", "humidity", "pressure"].forEach((dataType) => {
if(payload.data[dataType]) {
payload.data[dataType] = parseInt(payload.data[dataType])/100;
}
});
}
else {
msg = null;
}
}
// Prepare for request
else {
miDevicesUtils.prepareForGatewayRequest(this, msg);
}
this.send(msg);
});
}
}
RED.nodes.registerType("xiaomi-ht", XiaomiHtNode);
};

View File

@@ -1,120 +0,0 @@
<script type="text/javascript">
RED.nodes.registerType('xiaomi-magnet', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"xiaomi-configurator"},
name: {value: ""},
sid: {value: "", required: true}
},
inputs: 1,
outputs: 1,
paletteLabel: "contact",
icon: "door-icon.png",
label: function () {
return this.name || "xiaomi-magnet";
},
oneditprepare: function() {
var node = this;
if(node.sid) {
$('#node-input-sid').val(node.sid);
}
function changeGateway(model) {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
$('#node-input-sid').empty();
for (key in configNode.deviceList) {
var device = configNode.deviceList[key];
if (device.model === model) {
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + '</option>');
}
}
if(node.sid) {
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
}
}
}
}
$("#node-input-sid").change(function () {
if(!this.name) {
$("#node-input-name").val($('#node-input-sid option:selected').text());
}
});
$("#node-input-gateway").change(function () {
changeGateway("magnet");
});
},
oneditsave: function() {
var node = this;
node.sid = $("#node-input-sid").val();
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-magnet">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-magnet">
<p>The Xiaomi contact sensor node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
<hr>
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample message:</p>
<p><pre>{
cmd: "read_ack"
model: "sensor_magnet.aq2"
sid: "158d000112fb5d"
short_id: 50301
data: {
voltage: 3015,
status: "close",
batteryLevel: 23
}
}</pre>
Where <code>status</code> can be <code>"open"</code> or <code>"close"</code>, <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
</script>

View File

@@ -1,9 +0,0 @@
const miDevicesUtils = require('../src/utils');
module.exports = (RED) => {
// magnet, sensor_magnet.aq2
function XiaomiMagnetNode(config) {
miDevicesUtils.defaultNode(RED, config, this);
}
RED.nodes.registerType("xiaomi-magnet", XiaomiMagnetNode);
};

View File

@@ -1,122 +0,0 @@
<script type="text/javascript">
RED.nodes.registerType('xiaomi-motion', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"xiaomi-configurator"},
name: {value: ""},
sid: {value: "", required: true},
motionmsg: {value: ""},
nomotionmsg: {value: ""},
output: {value: "0"}
},
inputs: 1,
outputs: 1,
paletteLabel: "motion",
icon: "motion-icon.png",
label: function () {
return this.name || "xiaomi-motion";
},
oneditprepare: function() {
var node = this;
if(node.sid) {
$('#node-input-sid').val(node.sid);
}
function changeGateway(model) {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
$('#node-input-sid').empty();
for (key in configNode.deviceList) {
var device = configNode.deviceList[key];
if (device.model === model) {
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + '</option>');
}
}
if(node.sid) {
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
}
}
}
}
$("#node-input-sid").change(function () {
if(!this.name) {
$("#node-input-name").val($('#node-input-sid option:selected').text());
}
});
$("#node-input-gateway").change(function () {
changeGateway("motion");
});
},
oneditsave: function() {
var node = this;
node.sid = $("#node-input-sid").val();
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-motion">
<div class="form-row">
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-motion">
<p>The Xiaomi body motion sensor node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
<hr>
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample message:</p>
<p><pre>{
cmd: "read_ack"
model: "motion"
sid: "158d00015ef56c"
short_id: 21672
data: {
voltage: 3035,
status: "motion",
batteryLevel: 45
}
}</pre>
Where <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
</script>

View File

@@ -1,9 +0,0 @@
const miDevicesUtils = require('../src/utils');
module.exports = (RED) => {
// motion
function XiaomiMotionNode(config) {
miDevicesUtils.defaultNode(RED, config, this);
}
RED.nodes.registerType("xiaomi-motion", XiaomiMotionNode);
};

Some files were not shown because too many files have changed in this diff Show More