2
0

refactor(gateway): rewrite the gateway part

This commit is contained in:
Pierre CLEMENT
2018-02-18 22:30:25 +01:00
parent c1299336cb
commit f4d54d714b
73 changed files with 1652 additions and 1171 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,7 +1,6 @@
# node-red-contrib-mi-devices
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:
@@ -15,9 +14,9 @@ The following devices are currently supported:
* Power plug (zigbee)
* Power plug (wifi)
* 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 +29,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 +94,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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,179 +1,9 @@
<!-- The Read Node -->
<script type="text/x-red" data-template-name="xiaomi-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">
<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>
<dd>Device <code>sid</code> to ask the report.</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 read',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "read",
icon: "mi-read.png",
label: function() {
return this.name||"read";
}
});
</script>
<!-- The get ids Node -->
<script type="text/x-red" data-template-name="xiaomi-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">
<p>Ask the gateway to the list of devices ids.</p>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions get_id_list',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "get id list",
icon: "mi-list.png",
label: function() {
return this.name||"get id list";
}
});
</script>
<!-- The Single click Node -->
<script type="text/x-red" data-template-name="xiaomi-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">
<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>
<dd>Device <code>sid</code> to ask the report.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions click',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "click",
icon: "mi-click.png",
label: function() {
return this.name||"click";
}
});
</script>
<!-- The Double click Node -->
<script type="text/x-red" data-template-name="xiaomi-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">
<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>
<dd>Device <code>sid</code> to ask the report.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
</script>
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions double_click',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "double click",
icon: "mi-double-click.png",
label: function() {
return this.name||"double click";
}
});
</script>
<!-- The Gateway light Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions gateway_light', {
RED.nodes.registerType('mi-devices-actions gateway_light', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -203,7 +33,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 gateway_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 +48,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 gateway_light">
<p>Change the light of the gateway.</p>
<h3>Inputs</h3>
@@ -254,7 +84,7 @@
<!-- The Gateway sound Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions gateway_sound', {
RED.nodes.registerType('mi-devices-actions gateway_sound', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -272,7 +102,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_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">
@@ -311,7 +141,7 @@
</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_sound">
<p>Play a sound on the gateway.</p>
<h3>Inputs</h3>
@@ -335,7 +165,7 @@
<!-- 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: {
@@ -350,14 +180,14 @@
}
});
</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>
@@ -371,7 +201,7 @@
<!-- The "on" Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions on',{
RED.nodes.registerType('mi-devices-actions on',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -386,14 +216,14 @@
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions on">
<script type="text/x-red" data-template-name="mi-devices-actions 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="xiaomi-actions on">
<script type="text/x-red" data-help-name="mi-devices-actions on">
<p>
Turn input device to on.
</p>
@@ -407,7 +237,7 @@
<!-- The "off" Node -->
<script type="text/javascript">
RED.nodes.registerType('xiaomi-actions off',{
RED.nodes.registerType('mi-devices-actions off',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
@@ -422,14 +252,14 @@
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions off">
<script type="text/x-red" data-template-name="mi-devices-actions 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 off">
<script type="text/x-red" data-help-name="mi-devices-actions off">
<p>
Turn input device to off.
</p>
@@ -443,7 +273,7 @@
<!-- 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: {
@@ -458,14 +288,14 @@
}
});
</script>
<script type="text/x-red" data-template-name="xiaomi-actions toggle">
<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 toggle">
<script type="text/x-red" data-help-name="mi-devices-actions toggle">
<p>Toggle device.</p>
<h3>Outputs</h3>

View File

@@ -14,7 +14,7 @@ module.exports = (RED) => {
}
});
}
RED.nodes.registerType("xiaomi-actions read", XiaomiActionRead);
RED.nodes.registerType("mi-devices-actions read", XiaomiActionRead);
/*********************************************
Get registred ids of devices on gateway
@@ -27,7 +27,7 @@ module.exports = (RED) => {
node.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions get_id_list", XiaomiActionGetIdList);
RED.nodes.registerType("mi-devices-actions get_id_list", XiaomiActionGetIdList);
/*********************************************
Virtual single click on a button
@@ -43,7 +43,7 @@ module.exports = (RED) => {
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions click", XiaomiActionSingleClick);
RED.nodes.registerType("mi-devices-actions click", XiaomiActionSingleClick);
/*********************************************
Virtual Double click on a button
@@ -59,7 +59,7 @@ module.exports = (RED) => {
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions double_click", XiaomiActionDoubleClick);
RED.nodes.registerType("mi-devices-actions double_click", XiaomiActionDoubleClick);
/*********************************************
Set the gateway light
@@ -88,7 +88,7 @@ module.exports = (RED) => {
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions gateway_light", XiaomiActionGatewayLight);
RED.nodes.registerType("mi-devices-actions gateway_light", XiaomiActionGatewayLight);
/*********************************************
Play a sound on the gateway
@@ -110,7 +110,7 @@ module.exports = (RED) => {
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions gateway_sound", XiaomiActionGatewaySound);
RED.nodes.registerType("mi-devices-actions gateway_sound", XiaomiActionGatewaySound);
/*********************************************
Stop playing a sound on the gateway
@@ -126,7 +126,7 @@ module.exports = (RED) => {
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions gateway_stop_sound", XiaomiActionGatewayStopSound);
RED.nodes.registerType("mi-devices-actions gateway_stop_sound", XiaomiActionGatewayStopSound);
/*********************************************
Turn device on
@@ -147,7 +147,7 @@ module.exports = (RED) => {
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions on", XiaomiActionPowerOn);
RED.nodes.registerType("mi-devices-actions on", XiaomiActionPowerOn);
/*********************************************
Turn device off
@@ -168,7 +168,7 @@ module.exports = (RED) => {
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions off", XiaomiActionPowerOff);
RED.nodes.registerType("mi-devices-actions off", XiaomiActionPowerOff);
/*********************************************
Toggle device
@@ -181,5 +181,5 @@ module.exports = (RED) => {
this.send(msg);
});
}
RED.nodes.registerType("xiaomi-actions toggle", XiaomiActionToggle);
RED.nodes.registerType("mi-devices-actions toggle", XiaomiActionToggle);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

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);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

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);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

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);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

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);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

View File

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

View File

@@ -1,23 +1,28 @@
{
"name": "node-red-contrib-mi-devices",
"version": "1.1.0",
"description": "A set of nodes to control some of the popular Xiaomi sensors which are connected to the Xiaomi Gateway, and the Gateway itself.",
"version": "2.0.0",
"description": "A set of nodes to control some of the popular Xiaomi sensors which are connected to the Xiaomi Gateway, the Gateway itself and Yeelights.",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com:pierrecle/node-red-contrib-mi-devices.git"
},
"publishConfig": {
"access": "public"
},
"config": {
"nodes_prefix": "mi-devices"
},
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && npm run build:ts && npm run build:ejs && npm run build:icons",
"build:ts": "tsc --allowUnreachableCode -p .",
"build:ejs": "npm run build:ejs:indexes && npm run build:ejs:devices",
"build:ejs:indexes": "ejs-cli --base-dir src/ --options \"{\\\"NODES_PREFIX\\\": \\\"mi-devices\\\"}\" \"**/index.ejs\" --out dist/",
"build:ejs:devices": "ejs-cli --base-dir src/ --options \"{\\\"NODES_PREFIX\\\": \\\"mi-devices\\\"}\" \"nodes/devices/*.ejs\" --out dist/",
"build:icons": "npm run build:icons:gateway && npm run build:icons:devices && npm run build:icons:actions && npm run build:icons:yeelight",
"build:ejs": "ejs-cli --base-dir src/ --options \"{\\\"NODES_PREFIX\\\": \\\"${npm_package_config_nodes_prefix}\\\"}\" \"**/index.ejs\" --out dist/",
"build:icons": "npm run build:icons:gateway && npm run build:icons:gateway-subdevices && npm run build:icons:actions && npm run build:icons:yeelight",
"build:icons:gateway": "cp -pr icons/gateway dist/nodes/gateway/icons",
"build:icons:devices": "cp -pr icons/devices dist/nodes/devices/icons",
"build:icons:gateway-subdevices": "cp -pr icons/gateway-subdevices dist/nodes/gateway-subdevices/icons",
"build:icons:actions": "cp -pr icons/actions dist/nodes/actions/icons",
"build:icons:yeelight": "cp -pr icons/yeelight dist/nodes/yeelight/icons"
"build:icons:yeelight": "cp -pr icons/yeelight dist/nodes/yeelight/icons",
"build:icons:plug-wifi": "cp -pr icons/plug-wifi dist/nodes/plug-wifi/icons"
},
"license": "MIT",
"keywords": [
@@ -28,16 +33,11 @@
],
"node-red": {
"nodes": {
"xiaomi-ht": "dist/nodes/devices/Sensor.js",
"xiaomi-magnet": "dist/nodes/devices/Magnet.js",
"xiaomi-motion": "dist/nodes/devices/Motion.js",
"xiaomi-switch": "dist/nodes/devices/Switch.js",
"xiaomi-socket": "node-red-contrib-xiaomi-socket/xiaomi-socket.js",
"xiaomi-socket-wifi": "node-red-contrib-xiaomi-socket-wifi/xiaomi-socket-wifi.js",
"xiaomi-all": "dist/nodes/devices/All.js",
"xiaomi-gateway": "dist/nodes/gateway/index.js",
"xiaomi-actions": "node-red-contrib-xiaomi-actions/xiaomi-actions.js",
"xiaomi-yeelight": "dist/nodes/yeelight/index.js"
"mi-devices-gateway": "dist/nodes/gateway/index.js",
"mi-devices-gateway-subdevices": "dist/nodes/gateway-subdevices/index.js",
"mi-devices-plug-wifi": "dist/nodes/plug-wifi/index.js",
"mi-devices-yeelight": "dist/nodes/yeelight/index.js",
"mi-devices-actions": "dist/nodes/actions/index.js"
}
},
"author": "Pierre CLEMENT",
@@ -47,7 +47,7 @@
"dependencies": {
"cryptojs": "^2.5.3",
"lumi-aqara": "^1.4.0",
"miio": "^0.14.1",
"miio": "^0.15.2",
"yeelight-wifi": "^2.3.0"
},
"engines": {

90
src/devices/Gateway.ts Normal file
View File

@@ -0,0 +1,90 @@
import * as events from 'events';
import * as crypto from 'crypto';
import {GatewayServer} from "./GatewayServer";
import {GatewayMessage, GatewaySubdevice, Magnet, Motion, Switch, Weather} from "./";
import * as MessageData from "./GatewayMessageData";
export class Gateway extends events.EventEmitter {
static iv: Buffer = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
protected lastToken: string;
protected password: string;
private _subdevices: { [sid: string]: GatewaySubdevice } = {};
constructor(public sid: string, public ip: string) {
super();
}
get key(): string {
var cipher = crypto.createCipheriv('aes-128-cbc', this.password, Gateway.iv);
var key = cipher.update(this.lastToken, "ascii", "hex");
cipher.final('hex');
return key;
}
handleMessage(msg: GatewayMessage) {
if (msg.data) {
if (msg.model === "gateway" && msg.sid === this.sid && msg.token) {
this.lastToken = msg.token;
}
}
if (msg.isGetIdListAck()) {
(<MessageData.GatewayMessageGetIdListData> msg.data).forEach((sid) => {
this.sendRead({cmd: "read", sid: sid});
});
}
if (msg.isReadAck() || msg.isReport()) {
if (!this._subdevices[msg.sid]) {
for (let SubDeviceClass of [Magnet, Motion, Switch, 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', (sid: string) => {
this.emit("subdevice-values-updated", sid);
});
this.emit("subdevice-found", msg.sid);
}
}
}
if (this._subdevices[msg.sid]) {
this._subdevices[msg.sid].handleMessage(msg);
}
}
}
getSubdevice(sid: string): GatewaySubdevice {
return this._subdevices[sid] || null;
}
hasSubdevice(sid: string): boolean {
return !!this._subdevices[sid];
}
setLight() {
}
playSound() {
}
sendRead(message: any) {
message.sid = message.sid || this.sid;
GatewayServer.getInstance().sendToGateway(this.sid, message);
}
get subdevices(): { [sid: string]: GatewaySubdevice } {
return this._subdevices;
}
toJSON() {
return {
sid: this.sid,
ip: this.ip,
key: this.password,
subdevices: this.subdevices
};
}
}

View File

@@ -0,0 +1,58 @@
import * as MessageData from './GatewayMessageData';
export interface GatewayRawMessage {
cmd: string;
sid: string;
short_id: string | number;
model: string;
port?: string;
ip?: string;
token?: string;
data?: string;
}
export class GatewayMessage {
cmd: string;
sid: string;
short_id: string | number;
model: string;
ip?: string;
token?: string;
port?: number;
data?: MessageData.GatewayMessageHeartbeatData
| MessageData.GatewayMessageGetIdListData
| MessageData.GatewayMessageReadAckMagnetData
| MessageData.GatewayMessageReadAckReportWeatherData
| MessageData.GatewayMessageDefaultSubdeviceData
| any;
constructor(raw: GatewayRawMessage) {
Object.assign(this, raw);
if (raw.port) {
this.port = parseInt(raw.port);
}
if (raw.data) {
this.data = JSON.parse(raw.data) || raw.data;
}
}
isHeartbeat(): boolean {
return this.cmd === "heartbeat";
}
isIam(): boolean {
return this.cmd === "iam";
}
isGetIdListAck(): boolean {
return this.cmd === "get_id_list_ack";
}
isReadAck(): boolean {
return this.cmd === "read_ack";
}
isReport(): boolean {
return this.cmd === "report";
}
}

View File

@@ -0,0 +1,20 @@
export interface GatewayMessageHeartbeatData {
ip: string;
}
export interface GatewayMessageGetIdListData extends Array<string> {
}
export interface GatewayMessageDefaultSubdeviceData {
voltage: number;
}
export interface GatewayMessageReadAckMagnetData extends GatewayMessageDefaultSubdeviceData {
status: string;
}
export interface GatewayMessageReadAckReportWeatherData extends GatewayMessageDefaultSubdeviceData {
temperature?: string;
humidity?: string;
pressure?: string;
}

View File

@@ -0,0 +1,165 @@
import * as events from 'events';
import * as dgram from "dgram";
import {Gateway} from "./Gateway";
import Timer = NodeJS.Timer;
import {GatewayMessage} from "./GatewayMessage";
export class GatewayServer extends events.EventEmitter {
static MULTICAST_ADDRESS = '224.0.0.50';
static MULTICAST_PORT = 4321;
static SERVER_PORT = 9898;
private static instance: GatewayServer;
private server: dgram.Socket;
private _gateways: { [sid: string]: Gateway } = {};
private _gatewaysPing: { [sid: string]: Timer } = {};
static getInstance() {
if (!this.instance) {
this.instance = new GatewayServer();
}
return this.instance;
}
discover(ipv: number = 4) {
if (this.server) {
return;
}
this.server = dgram.createSocket(<dgram.SocketOptions> {
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(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 Gateway(msg.sid, remote.address);
this.sendToGateway(msg.sid, {cmd: "get_id_list"});
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);
/*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);
}*/
});
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: string): Gateway {
return this._gateways[sid] || null;
}
hasGateway(sid: string): boolean {
return !!this._gateways[sid];
}
get gateways(): { [sid: string]: Gateway } {
return this._gateways;
}
sendToGateway(sid: string, message: any) {
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);
}
}
}

View File

@@ -0,0 +1,45 @@
import * as events from 'events';
import {GatewayMessage} from "./GatewayMessage";
import {GatewayMessageDefaultSubdeviceData} from "./GatewayMessageData";
export abstract class GatewaySubdevice extends events.EventEmitter {
public voltage: number;
public message: GatewayMessage;
constructor(public sid: string, public model: string) {
super();
}
get batteryLevel(): number {
/*
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: GatewayMessage): void {
this.voltage = (<GatewayMessageDefaultSubdeviceData> msg.data).voltage;
this.message = msg;
}
static get acceptedModels(): string[] {
return [];
};
abstract get internalModel(): string;
toJSON() {
let json:any = {};
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;
}
}

39
src/devices/Magnet.ts Normal file
View File

@@ -0,0 +1,39 @@
import {GatewaySubdevice} from "./GatewaySubdevice";
import {GatewayMessage} from "./GatewayMessage";
import {GatewayMessageReadAckMagnetData} from "./GatewayMessageData";
export class Magnet extends GatewaySubdevice {
status: string;
static get acceptedModels(): string[] {
return ['magnet', 'sensor_magnet.aq2'];
}
get internalModel(): string {
return 'mi.magnet';
}
isClosed(): boolean {
return this.status === "close";
}
isOpened(): boolean {
return this.status === "open";
}
isUnkownState(): boolean {
return this.status === "unkown";
}
handleMessage(msg: GatewayMessage) {
super.handleMessage(msg);
if (msg.isReadAck() || msg.isReport()) {
let data = <GatewayMessageReadAckMagnetData> msg.data;
// mintime
if (this.status !== data.status) {
this.status = data.status;
this.emit('values-updated', this.sid);
}
}
}
}

17
src/devices/Motion.ts Normal file
View File

@@ -0,0 +1,17 @@
import {GatewaySubdevice} from "./GatewaySubdevice";
import {GatewayMessage} from "./GatewayMessage";
export class Motion extends GatewaySubdevice {
static get acceptedModels():string[] {
return ['motion'];
}
get internalModel(): string {
return 'mi.motion';
}
handleMessage(msg: GatewayMessage) {
super.handleMessage(msg);
}
}

17
src/devices/Switch.ts Normal file
View File

@@ -0,0 +1,17 @@
import {GatewaySubdevice} from "./GatewaySubdevice";
import {GatewayMessage} from "./GatewayMessage";
export class Switch extends GatewaySubdevice {
static get acceptedModels():string[] {
return ['switch', 'sensor_switch.aq2'];
}
get internalModel(): string {
return 'mi.switch';
}
handleMessage(msg: GatewayMessage) {
super.handleMessage(msg);
}
}

49
src/devices/Weather.ts Normal file
View File

@@ -0,0 +1,49 @@
import {GatewaySubdevice} from "./GatewaySubdevice";
import {GatewayMessage} from "./GatewayMessage";
import {GatewayMessageReadAckReportWeatherData} from "./GatewayMessageData";
export class Weather extends GatewaySubdevice {
temperature: number;
humidity: number;
/**
* Pressure in Pascals
*/
pressure: number;
static get acceptedModels():string[] {
return ['sensor_ht', 'weather.v1'];
}
get internalModel(): string {
return 'mi.weather';
}
get temperatureInDegrees(): number {
return this.temperature / 100;
}
get humidityInPercent(): number {
return this.humidity / 100;
}
get pressureInBar(): number {
return this.pressure / 100000;
}
get pressureInhPa(): number {
return this.pressure / 100;
}
handleMessage(msg: GatewayMessage) {
super.handleMessage(msg);
if (msg.isReadAck() || msg.isReport()) {
let data = <GatewayMessageReadAckReportWeatherData> msg.data;
['temperature', 'humidity', 'pressure'].forEach((dataType: string) => {
if (data[dataType]) {
this[dataType] = parseInt(data[dataType]);
}
});
this.emit('values-updated', this.sid);
}
}
}

8
src/devices/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export * from './Gateway';
export * from './GatewayMessage';
export * from './GatewayServer';
export * from './GatewaySubdevice';
export * from './Magnet';
export * from './Motion';
export * from './Switch';
export * from './Weather';

View File

@@ -0,0 +1,39 @@
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-actions <%= type %>">
<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="<%= NODES_PREFIX %>-actions <%= type %>">
<p><%= docTitle %></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><%= NODES_PREFIX %>-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('<%= NODES_PREFIX %>-actions <%= type %>',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "<%= label %>",
icon: "<%= icon %>.png",
label: function() {
return this.name||"<%= NODES_PREFIX %> <%= label %>";
}
});
</script>

View File

@@ -0,0 +1,79 @@
<!-- The Gateway light Node -->
<script type="text/javascript">
RED.nodes.registerType('<%= NODES_PREFIX %>-actions gateway_light', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value: ""},
brightness: {value: 100},
hexRgbColor: {value: "#ffffff"},
color: {value:{red: 255, green: 255, blue: 255}}
},
inputs: 1,
outputs: 1,
paletteLabel: "set light",
icon: "mi-bulb.png",
label: function () {
return this.name || "set light";
},
oneditsave: function() {
var hexRgbColor = $("#node-input-hexRgbColor").val();
var split = hexRgbColor.slice(1).match(/.{1,2}/g).map(function(hexColor) {
return parseInt(hexColor, 16);
});
this.color = {
red: split[0],
green: split[1],
blue: split[2]
};
}
});
</script>
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-actions gateway_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">
</div>
<div class="form-row">
<label for="node-input-brightness"><i class="icon-tag"></i> Brightness</label>
<input type="range" id="node-input-brightness" min="0" max="100">
</div>
<div class="form-row">
<label for="node-input-hexRgbColor"><i class="icon-tag"></i> Color</label>
<input type="color" id="node-input-hexRgbColor">
</div>
</script>
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-actions gateway_light">
<p>Change the light of the gateway.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>brightness
<span class="property-type">number</span>
</dt>
<dd>The brightness value between <code>0</code> and <code>100</code>.</dd>
<dt>color
<span class="property-type">object</span>
</dt>
<dd>The color itself. This object must contain the followinf properties:
<ul>
<li>
<code>red</code> - amout of red, between <code>0</code> and <code>255</code>
</li>
<li>
<code>green</code> - amout of green, between <code>0</code> and <code>255</code>
</li>
<li>
<code>blue</code> - amout of blue, between <code>0</code> and <code>255</code>
</li>
</ul>
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
</script>

View File

@@ -0,0 +1,34 @@
import { Red, NodeProperties } from "node-red";
import { Constants } from "../constants";
export default (RED:Red) => {
class GatewayLight {
public color:string;
public brightness:number;
constructor(props:NodeProperties) {
RED.nodes.createNode(<any> this, props);
(<any> this).setListeners();
}
protected setListeners() {
(<any> this).on('input', (msg) => {
let color = msg.color || this.color;
let brightness = msg.brightness || this.brightness;
if(msg.sid) {
msg.payload = {
cmd: "write",
data: { rgb: 123, sid: msg.sid }
};
}
else {
msg.payload = {
brightness: brightness
};
}
(<any> this).send(msg);
});
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-actions gateway_light`, <any> GatewayLight);
};

View File

@@ -0,0 +1,22 @@
import { Red, NodeProperties, NodeStatus } from "node-red";
import { Constants } from "../constants";
export default (RED:Red, type:string) => {
class ReadAction {
constructor(props:NodeProperties) {
RED.nodes.createNode(<any> this, props);
(<any> this).on('input', (msg) => {
if(msg.sid) {
msg.payload = {
cmd: (<any> this).type.replace(`${Constants.NODES_PREFIX}-actions `, ''),
sid: msg.sid
};
(<any> this).send(msg);
}
});
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-actions ${type}`, <any> ReadAction);
};

View File

@@ -0,0 +1,24 @@
import { Red, NodeProperties, NodeStatus } from "node-red";
import { Constants } from "../constants";
export default (RED:Red, type:string) => {
class WriteAction {
constructor(props:NodeProperties) {
RED.nodes.createNode(<any> this, props);
(<any> this).on('input', (msg) => {
if(msg.sid) {
msg.payload = {
cmd: "write",
data: {
status: (<any> this).type.replace(`${Constants.NODES_PREFIX}-actions `, ''),
sid: msg.sid
}
};
(<any> this).send(msg);
}
});
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-actions ${type}`, <any> WriteAction);
};

View File

@@ -0,0 +1,33 @@
<%# ---------------------------------- read ---------------------------------- %>
<%- include('./Action', {
type: "read",
label: "read",
icon: "mi-read",
docTitle: "Ask the gateway to read the report of the input device."
}) %>
<%# ---------------------------------- get_id_list ---------------------------------- %>
<%- include('./Action', {
type: "get_id_list",
label: "get id list",
icon: "mi-list",
docTitle: "Ask the gateway to the list of devices ids."
}) %>
<%# ---------------------------------- click ---------------------------------- %>
<%- include('./Action', {
type: "click",
label: "click",
icon: "mi-click",
docTitle: "Virtual single click for switch."
}) %>
<%# ---------------------------------- double_click ---------------------------------- %>
<%- include('./Action', {
type: "double_click",
label: "double click",
icon: "double-click",
docTitle: "Virtual double click for switch."
}) %>
<%- include('./GatewayLight', {}) %>

View File

@@ -0,0 +1,17 @@
import { Red, NodeProperties } from "node-red";
import * as LumiAqara from 'lumi-aqara';
import {default as ReadAction} from './ReadAction';
import {default as WriteAction} from './WriteAction';
import {default as GatewayLight} from './GatewayLight';
export = (RED:Red) => {
GatewayLight(RED);
["read", "get_id_list"].forEach((action) => {
ReadAction(RED, action);
});
["click", "double_click"].forEach((action) => {
WriteAction(RED, action);
});
};

View File

@@ -1,3 +1,6 @@
export class Constants {
static readonly NODES_PREFIX = "mi-devices";
static get NODES_PREFIX(){
let packageJson = require(`${__dirname}/../../package`);
return packageJson.config.nodes_prefix;
};
}

View File

@@ -0,0 +1,124 @@
<script type="text/javascript">
RED.nodes.registerType('<%= NODES_PREFIX %>-all', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"<%= NODES_PREFIX %>-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 (key in configNode.deviceList) {
var device = configNode.deviceList[key];
if (onlyModels.length == 0 || onlyModels.indexOf(device.model) >= 0) {
var option = $('<option value="' + device.sid + '">' + device.desc + '</option>');
if(excludedSids && excludedSids.indexOf(device.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="<%= NODES_PREFIX %>-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="sensor_ht,weather.v1">Temperature/humidty</option>
<option value="motion">Motion</option>
<option value="switch,sensor_switch.aq2">Switches</option>
<option value="magnet,sensor_magnet.aq2">Contacts</option>
<option value="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="<%= NODES_PREFIX %>-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>

View File

@@ -0,0 +1,60 @@
import { Red, NodeProperties } from "node-red";
import { Constants } from "../constants";
export default (RED:Red) => {
class All {
protected gateway: any;
protected onlyModels: string[];
protected excludedSids: string[];
static getOnlyModelsValue(input) {
var cleanOnlyModels = [];
input.forEach((value) => {
cleanOnlyModels = cleanOnlyModels.concat(value.split(','));
});
return cleanOnlyModels;
}
constructor(props:NodeProperties) {
RED.nodes.createNode(<any> this, props);
this.gateway = RED.nodes.getNode((<any> props).gateway);
this.onlyModels = All.getOnlyModelsValue((<any> props).onlyModels || []);
this.excludedSids = (<any> props).excludedSids;
}
protected setMessageListener() {
(<any> this).on('input', (msg) => {
if (this.gateway) {
// 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));
}
(<any> this).send(msg);
}
});
}
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;
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-all`, <any> All);
};

View File

@@ -1,18 +1,18 @@
<script type="text/javascript">
RED.nodes.registerType('xiaomi-switch', {
RED.nodes.registerType('<%= NODES_PREFIX %>-<%= type %>', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"xiaomi-configurator"},
gateway: {value:"", type:"<%= NODES_PREFIX %>-gateway configurator"},
name: {value: ""},
sid: {value: "", required: true}
},
inputs: 1,
outputs: 1,
paletteLabel: "switch",
icon: "mi-switch.png",
paletteLabel: "<%= label %>",
icon: "<%= icon %>.png",
label: function () {
return this.name || "xiaomi-switch";
return this.name || "<%= NODES_PREFIX %> <%= type %>";
},
oneditprepare: function() {
var node = this;
@@ -25,12 +25,13 @@
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
console.log(model, configNode);
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 (device.type === model) {
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + ' - ' + device.sid + '</option>');
}
}
if(node.sid) {
@@ -46,11 +47,8 @@
}
});
$("#node-input-gateway").change(function () {
changeGateway("switch");
changeGateway("<%= filterType %>");
});
$(".node-input-msg").hide();
$("#node-input-output").val(node.output);
},
oneditsave: function() {
var node = this;
@@ -59,7 +57,7 @@
});
</script>
<script type="text/x-red" data-template-name="xiaomi-switch">
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-<%= type %>">
<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">
@@ -74,8 +72,8 @@
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-switch">
<p>The Xiaomi Switch node</p>
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-<%= type %>">
<p><%= docTitle %> sensor node</p>
<h3>Inputs</h3>
<dl class="message-properties">
@@ -85,9 +83,7 @@
<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>.
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>
@@ -99,22 +95,14 @@
<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>
<dd>The <code><%= NODES_PREFIX %>-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 message:</p>
<p><pre>{
cmd: "report"
model: "switch"
sid: "158d000128b124"
short_id: 56773
data: {
status: "click",
batteryLevel: 23
}
}</pre></p>
</script>
<p>Sample payload after incoming incoming message:</p>
<p><pre><%- incomingSample %></pre>
Where <%= (!!locals.incomingDetails)?incomingDetails + ',':'' %> <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
</script>

View File

@@ -0,0 +1,49 @@
import { Red, NodeProperties, NodeStatus } from "node-red";
import { Constants } from "../constants";
export default (RED:Red, type:string) => {
class GatewayDevice {
protected gateway: any;
protected sid: string;
constructor(props:NodeProperties) {
RED.nodes.createNode(<any> this, props);
this.gateway = RED.nodes.getNode((<any> props).gateway);
(<any> this).status({fill:"grey", shape:"ring", text:"battery - na"});
if (this.gateway) {
(<any> this).on('input', (msg) => {
let payload = msg.payload;
// Input from gateway
if (payload.sid) {
if (payload.sid == this.sid) {
let batteryLevel = payload.getBatteryPercentage();
var status:NodeStatus = {
fill: "green", shape: "dot",
text: "battery - " + batteryLevel + "%"
};
if (batteryLevel < 10) {
status.fill = "red";
} else if (batteryLevel < 45) {
status.fill = "yellow";
}
(<any> this).status(status);
(<any> this).send([msg]);
}
}
// Prepare for request
else {
msg.sid = this.sid;
msg.gateway = this.gateway;
(<any> this).send(msg);
}
});
}
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-${type}`, <any> GatewayDevice);
};

View File

@@ -1,21 +1,21 @@
<script type="text/javascript">
RED.nodes.registerType('xiaomi-motion', {
RED.nodes.registerType('<%= NODES_PREFIX %>-plug', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"xiaomi-configurator"},
gateway: {value:"", type:"<%= NODES_PREFIX %>-gateway configurator"},
name: {value: ""},
sid: {value: "", required: true},
motionmsg: {value: ""},
nomotionmsg: {value: ""},
onmsg: {value: ""},
offmsg: {value: ""},
output: {value: "0"}
},
inputs: 1,
outputs: 1,
paletteLabel: "motion",
icon: "motion-icon.png",
paletteLabel: "plug (zigbee)",
icon: "outlet-icon.png",
label: function () {
return this.name || "xiaomi-motion";
return this.name || "xiaomi-plug";
},
oneditprepare: function() {
var node = this;
@@ -49,7 +49,7 @@
}
});
$("#node-input-gateway").change(function () {
changeGateway("motion");
changeGateway("plug");
});
},
oneditsave: function() {
@@ -59,7 +59,7 @@
});
</script>
<script type="text/x-red" data-template-name="xiaomi-motion">
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-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">
@@ -74,49 +74,53 @@
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-motion">
<p>The Xiaomi body motion sensor node</p>
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-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">object</span>
<span class="property-type">string | json</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>
<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">
<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>
<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 message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<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: "read_ack"
model: "motion"
sid: "158d00015ef56c"
short_id: 21672
data: {
voltage: 3035,
status: "motion",
batteryLevel: 45
cmd: "write_ack"
model: "plug"
sid: "158d00012f1fb5"
short_id: 47414
data: {
voltage:3600,
status:"off",
inuse:"0",
power_consumed:"4000",
load_power:"0"
}
}</pre>
Where <code>batteryLevel</code> is a computed percentage of remaining battery.
</p>
}</pre></p>
</script>

View File

@@ -0,0 +1,40 @@
import { Red, NodeProperties } from "node-red";
import { Constants } from "../constants";
export default (RED:Red) => {
class Plug {
protected gateway: any;
protected sid: string;
constructor(props:NodeProperties) {
RED.nodes.createNode(<any> this, props);
this.gateway = RED.nodes.getNode((<any> props).gateway);
(<any> this).status({fill:"grey", shape:"ring", text:"status"});
}
protected setListener() {
if (this.gateway) {
(<any> this).on('input', (msg) => {
var payload = msg.payload;
if(payload.sid) {
if (payload.sid == this.sid) {
if (payload.data.status && payload.data.status == "on") {
(<any> this).status({fill:"green", shape:"dot", text:"on"});
} else if (payload.data.status && payload.data.status == "off") {
(<any> this).status({fill:"red", shape:"dot", text:"off"});
}
(<any> this).send(msg);
}
}
// Prepare for request
else {
(<any> this).send(msg);
}
});
}
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-plug`, <any> Plug);
}

View File

@@ -0,0 +1,85 @@
<%- include('./All', {}); %>
<%- include('./Plug', {}); %>
<%# ---------------------------------- Magnet ---------------------------------- %>
<%- include('./GatewaySubdevice', {
type: "magnet",
label: "contact",
icon: "door-icon",
filterType: "magnet",
docTitle: "The Xiaomi contact",
incomingSample: `{
cmd: "read_ack"
model: "sensor_magnet.aq2"
sid: "158d000112fb5d"
short_id: 50301
data: {
voltage: 3015,
status: "close",
batteryLevel: 23
}
}`,
incomingDetails: `<code>status</code> can be <code>"open"</code> or <code>"close"</code>`
}) %>
<%# ---------------------------------- Motion ---------------------------------- %>
<%- include('./GatewaySubdevice', {
type: "motion",
label: "motion",
icon: "motion-icon",
filterType: "motion",
docTitle: "The Xiaomi body motion",
incomingSample: `{
cmd: "read_ack"
model: "motion"
sid: "158d00015ef56c"
short_id: 21672
data: {
voltage: 3035,
status: "motion",
batteryLevel: 45
}
}`
}) %>
<%# ---------------------------------- Sensor HT ---------------------------------- %>
<%- include('./GatewaySubdevice', {
type: "ht",
label: "sensor HT",
icon: "thermometer-icon",
filterType: "sensor_ht",
docTitle: "The Xiaomi Humidity & Temperature",
incomingSample: `{
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
}
}`,
incomingDetails: `<code>humidy</code> is in percents, <code>pressure</code> in kPa`
}) %>
<%# ---------------------------------- Switch ---------------------------------- %>
<%- include('./GatewaySubdevice', {
type: "switch",
label: "switch",
icon: "mi-switch",
filterType: "switch",
docTitle: "The Xiaomi Switch",
incomingSample: `{
cmd: "report"
model: "switch"
sid: "158d000128b124"
short_id: 56773
data: {
status: "click",
batteryLevel: 23
}
}`
}) %>

View File

@@ -0,0 +1,14 @@
import { Red, NodeProperties } from "node-red";
import * as LumiAqara from 'lumi-aqara';
import {default as All} from "./All";
import {default as Plug} from "./Plug";
import {default as GatewaySubdevice} from "./GatewaySubdevice";
export = (RED:Red) => {
All(RED);
Plug(RED);
["magnet", "motion", "sensor", "switch"].forEach((subdeviceType) => {
GatewaySubdevice(RED, subdeviceType);
});
};

View File

@@ -3,64 +3,105 @@
category: 'config',
defaults: {
name: {value: ""},
ip: {value: ""},
sid: {value: ""},
key: { value: "" },
deviceList: {value:{}}
},
credentials: {
key: { type: "text" }
},
paletteLabel: "gateway configurator",
label: function () {
return this.name || "gateway configurator";
},
oneditprepare: function() {
RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways.forEach(function(gateway, index) {
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;
function addSubdevice(device) {
var devicesConfig = {
"sensor": {value:"sensor", label:"sensor ht", icon:"icons/node-red-contrib-mi-devices/thermometer-icon.png"},
"magnet": {value:"magnet", label:"magnet", icon:"icons/node-red-contrib-mi-devices/door-icon.png"},
"motion": {value:"motion", label:"motion", icon:"icons/node-red-contrib-mi-devices/motion-icon.png"},
"switch": {value:"switch", label:"switch", icon:"icons/node-red-contrib-mi-devices/mi-switch.png"},
"plug": {value:"plug", label:"plug zigbee", icon:"icons/node-red-contrib-mi-devices/outlet-icon.png"}
};
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"}
};
var row = $('<div/>', {class: "form-row"}).appendTo($('#input-subdevices'));
$('<input/>', {value: device.sid, type: "hidden", name: "sid"}).appendTo(row);
$('<input/>', {value: device.type, type: "hidden", name: "type"}).appendTo(row);
$('<label/>', {for: "node-config-input-name-" + device.sid}).html('<img src="' + devicesConfig[device.type].icon + '" style="width:24px;height:24px;filter:contrast(0);"> ' + device.sid).appendTo(row);
$('<input/>', {id: "node-config-input-name-" + device.sid, type: "text", value: (node.deviceList && node.deviceList[device.sid].name) || devicesConfig[device.type].label}).appendTo(row);
}
$("#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: '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.filter(function(e) { return e.sid == sid })[0];
var gateway = sid && RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways[sid];
$("#node-config-input-sid").val(gateway && gateway.sid);
$("#node-config-input-ip").val("");
$("#node-config-input-key").val("");
$("#node-config-input-key").val(gateway && gateway.key);
gateway && gateway.subdevices.forEach(function(device) {
addSubdevice(device);
$("#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) {
console.log(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', device);
});
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,
type: device.internalModel,
name: device.name
});
});
$.each(this.deviceList, function(sid, elt) {
addSubdevice({sid: sid, type: elt.type})
});
var listHeight = $("#node-config-input-subdevices").editableList('items').size() * 51 + 50;
$("#node-config-input-subdevices").editableList('height', listHeight);
},
oneditsave: function() {
var node = this;
$('#input-subdevices > *').each(function(i, elt) {
var sid = $(elt).find('input[name=sid]').val();
var type = $(elt).find('input[name=type]').val();
var name = $(elt).find('#node-config-input-name-' + sid).val() || "";
node.deviceList[sid] = {type: type, name: name};
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 model = deviceElement.find("#node-config-input-sid-"+i).typedInput('type');
node.deviceList[sid] = {type: model, name: desc};
});
}
});
@@ -79,21 +120,18 @@
<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-ip"><i class="fa fa-compass"></i> IP</label>
<input type="text" id="node-config-input-ip" placeholder="IP">
</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-key"><i class="fa fa-key"></i> Key</label>
<input type="text" id="node-input-key" placeholder="Key">
<label for="node-input-gatewayKey"><i class="fa fa-key"></i> Key</label>
<input type="text" id="node-config-input-key" placeholder="Key">
</div>
<p>Note: use <code>ip</code> or <code>sid</code> - <code>sid</code> is better.</p>
<h4>Devices</h4>
<div id="input-subdevices"></div>
<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="<%= NODES_PREFIX %>-gateway configurator">

View File

@@ -1,44 +1,73 @@
import { Red, Node, NodeProperties, NodeStatus, ClearNodeStatus } from "node-red";
import { Constants } from "../constants";
import { Searcher } from "./Searcher";
import { LumiAqara } from "../../../typings/index";
import {Red, Node, NodeProperties} from "node-red";
import {Constants} from "../constants";
import {GatewayServer} from "../../devices/GatewayServer";
import {Gateway} from "../../devices/Gateway";
import {GatewaySubdevice} from "../../devices/GatewaySubdevice";
export interface IGatewayConfiguratorNode extends Node {
ip:string;
sid:number;
gateway: LumiAqara.Gateway;
ip: string;
sid: number;
gateway: Gateway;
on(event: "gatewayFound", listener: () => void): any;
on(event: "gateway-online", listener: (sid: string) => void): any;
on(event: "gateway-offline", listener: (sid: string) => void): any;
on(event: "subdevice-update", listener: (subdevice: GatewaySubdevice) => void): any;
}
export default (RED:Red) => {
export default (RED: Red) => {
class GatewayConfigurator {
ip:string;
sid:number;
_gateway:LumiAqara.Gateway;
sid: string;
key: string;
_gateway: Gateway;
constructor(props: NodeProperties) {
RED.nodes.createNode(<any> this, props);
let {ip, sid} = <any> props;
let {sid, key} = <any> props;
this.sid = sid;
this.ip = ip;
this.key = key;
let server = GatewayServer.getInstance();
if (this.sid) {
this.setGateway();
}
server.on('gateway-online', (sid: string) => {
if (sid === this.sid) {
this.setGateway();
(<any> this).emit('gateway-online');
}
});
server.on('gateway-offline', (sid: string) => {
if (sid === this.sid) {
this._gateway = null;
(<any> this).emit('gateway-offline');
}
});
}
set gateway(gateway:LumiAqara.Gateway) {
this._gateway = gateway;
this._gateway.setPassword((<any> this).credentials.key);
(<any> this).emit('gatewayFound');
protected setGateway() {
this._gateway = GatewayServer.getInstance().getGateway(this.sid);
this._gateway && this._gateway.on("subdevice-values-updated", (sid: string) => {
let subdevice = this._gateway.getSubdevice(sid);
if (subdevice) {
(<any> this).emit('subdevice-update', subdevice);
}
});
}
get gateway() {
get gateway(): Gateway {
return this._gateway;
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-gateway configurator`, <any> GatewayConfigurator, {
settings: {
miDevicesGatewayConfiguratorDiscoveredGateways: { value: Searcher.gateways, exportable: true }
},
credentials: { key: {type:"text"} }
miDevicesGatewayConfiguratorDiscoveredGateways: {
value: GatewayServer.getInstance().gateways,
exportable: true
}
}
});
};

View File

@@ -1,26 +1,43 @@
import {Red, Node, NodeProperties} from 'node-red';
import {LumiAqara} from '../../../typings';
import { Constants } from '../constants';
import {Constants} from '../constants';
import {Gateway} from "../../devices/Gateway";
export interface IGatewayInNode extends Node {
gatewayConf:any;
gateway: LumiAqara.Gateway;
setGateway(gateway:LumiAqara.Gateway);
gatewayConf: any;
gateway: Gateway;
}
export default (RED:Red) => {
export default (RED: Red) => {
class GatewayIn {
protected gatewayConf: any;
protected gateway: LumiAqara.Gateway;
constructor(props:NodeProperties) {
constructor(props: NodeProperties) {
RED.nodes.createNode(<any> this, props);
this.gatewayConf = RED.nodes.getNode((<any> props).gateway);
(<any>this).status({fill:"red", shape:"ring", text: "offline"});
(<any>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());
}
setGateway(gateway:LumiAqara.Gateway) {
protected gatewayOnline() {
(<any>this).status({fill: "blue", shape: "dot", text: "online"});
this.gatewayConf.on('subdevice-update', (subdevice) => {
(<any> this).send({payload: subdevice});
});
}
protected gatewayOffline() {
(<any>this).status({fill: "red", shape: "ring", text: "offline"});
}
/*setGateway(gateway:LumiAqara.Gateway) {
this.gateway = gateway;
this.gateway.setPassword(this.gatewayConf.password);
(<any>this).status({fill:"blue", shape:"dot", text: "online"});
@@ -63,7 +80,7 @@ export default (RED:Red) => {
payload: device
});
});
}
}*/
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-gateway in`, <any> GatewayIn);

View File

@@ -1,8 +1,4 @@
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-gateway out">
<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">
@@ -10,7 +6,7 @@
</script>
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-gateway out">
<p>This node sends <code>msg.payload</code> to the configured Xiaomi Gateway.</p>
<p>This node sends <code>msg.payload</code> to <code>msg.gateway</code> Xiaomi Gateway.</p>
</script>
<script type="text/javascript">
@@ -18,8 +14,7 @@
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""},
gateway: {value:"", type:"<%= NODES_PREFIX %>-gateway configurator"}
name: {value:""}
},
inputs:1,
outputs:0,
@@ -31,24 +26,6 @@
},
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,47 +1,44 @@
import { Red, NodeProperties } from "node-red";
import { LumiAqara } from "../../../typings/index";
import { Constants } from "../constants";
import {Gateway} from "../../devices/Gateway";
export interface IGatewayOutNode extends Node {
gatewayConf:any;
gateway: LumiAqara.Gateway;
setGateway(gateway:LumiAqara.Gateway);
gateway: Gateway;
}
export default (RED:Red) => {
class GatewayOut {
protected gatewayConf: any;
protected gateway: LumiAqara.Gateway;
constructor(props:NodeProperties) {
constructor(props: NodeProperties) {
RED.nodes.createNode(<any> this, props);
this.gatewayConf= RED.nodes.getNode((<any> props).gateway);
(<any> this).status({fill:"red", shape:"ring", text: "offline"});
this.gatewayConf = RED.nodes.getNode((<any> props).gateway);
this.setMessageListener();
(<any>this).status({fill: "red", shape: "ring", text: "offline"});
if (this.gatewayConf.gateway) {
(<any>this).status({fill: "blue", shape: "dot", text: "online"});
}
this.gatewayConf.on('gateway-online', () => {
(<any>this).status({fill: "blue", shape: "dot", text: "online"});
});
this.gatewayConf.on('gateway-offline', () => {
(<any>this).status({fill: "red", shape: "ring", text: "offline"});
});
}
protected setMessageListener() {
(<any> this).on("input", (msg) => {
/*(<any> this).on("input", (msg) => {
if (msg.hasOwnProperty("payload") && this.gateway) {
if(msg.payload.cmd === "write" && !msg.payload.data.key && this.gateway && this.gateway.sid && this.gateway._key) {
msg.payload.data.key = this.gateway._key;
}
this.gateway._sendUnicast(JSON.stringify(msg.payload));
}
});
}
setGateway(gateway) {
this.gateway = gateway;
this.gateway.setPassword(this.gatewayConf.password);
(<any> this).status({fill:"blue", shape:"dot", text: "online"});
this.gateway.on('offline', () => {
this.gateway = null;
(<any> this).status({fill:"red", shape:"ring", text: "offline"});
});
});*/
}
}

View File

@@ -1,42 +0,0 @@
import { Red } from "node-red";
import * as LumiAqara from 'lumi-aqara';
import { Constants } from "../constants";
import { IGatewayConfiguratorNode } from "./GatewayConfigurator";
export class Searcher {
static _gateways:LumiAqara.Gateway[] = [];
static discover(RED:Red) {
new Promise(() => {
const aqara = new LumiAqara();
aqara.on('gateway', (gateway:LumiAqara.Gateway) => {
let frontGateway = {
sid: gateway.sid,
ip: gateway.ip,
subdevices: []
};
this._gateways.push(frontGateway);
gateway.on('subdevice', (device:LumiAqara.SubDevice) => {
frontGateway.subdevices.push({
sid: device.getSid(),
type: device.getType()
});
});
RED.nodes.eachNode((tmpNode) => {
if(tmpNode.type.indexOf(`${Constants.NODES_PREFIX}-gateway configurator`) === 0) {
let tmpNodeInst = <IGatewayConfiguratorNode> RED.nodes.getNode(tmpNode.id);
if(tmpNodeInst && (tmpNodeInst.ip === gateway.ip || tmpNodeInst.sid === gateway.sid)) {
tmpNodeInst.gateway = gateway;
}
}
});
});
});
}
static get gateways():LumiAqara.Gateway {
return this._gateways;
}
}

View File

@@ -1,14 +1,14 @@
import { Red, NodeProperties } from "node-red";
import * as LumiAqara from 'lumi-aqara';
import { Searcher } from "./Searcher";
import { GatewayServer } from "../../devices/GatewayServer";
import {default as GatewayConfigurator} from "./GatewayConfigurator";
import {default as Gateway} from "./Gateway";
import {default as GatewayIn} from "./GatewayIn";
import {default as GatewayOut} from "./GatewayOut";
export = (RED:Red) => {
Searcher.discover(RED);
GatewayServer.getInstance().discover();
GatewayConfigurator(RED);
Gateway(RED);

View File

@@ -0,0 +1,67 @@
<script type="text/javascript">
RED.nodes.registerType('<%= NODES_PREFIX %>-plug-wifi', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
name: {value: ""},
ip: {value: "", required: true},
onmsg: {value: ""},
offmsg: {value: ""},
output: {value: "0"}
},
inputs: 1,
outputs: 1,
outputLabels: ["Status"],
paletteLabel: "plug (wifi)",
icon: "outlet-wifi-icon.png",
label: function () {
return this.name || "plug-wifi";
}
});
</script>
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-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">
</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 address">
</div>
</script>
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-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>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">string</span>
</dt>
<dd><code>on</code> or <code>off</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, see below.</dd>
</dl>
</ol>
<h4>Details</h4>
<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 full data:</p>
<p><pre>{
type: "power-plug",
model: "chuangmi.plug.m1",
capabilities: [ {"0": "power-channels"} ],
address: "192.168.178.31",
port: 54321,
power: { "0": false },
state: "on"
}</pre></p>
</script>

View File

@@ -0,0 +1,151 @@
import { Constants } from "../constants";
const miio = require("miio");
export = (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.NODES_PREFIX}-wifi-plug`, XiaomiPlugWifiNode);
}

View File

@@ -7,7 +7,7 @@
sid: {value: ""}
},
label: function () {
return this.name || "yeelight configurator";
return this.name || "yeelight conf";
},
oneditprepare: function() {
RED.settings.miDevicesYeelightConfiguratorDiscoveredBulbs.forEach(function(bulb, index) {