diff --git a/.gitignore b/.gitignore
index f872e90..ac753a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
-*.iml
+.DS_Store
+.idea
+dist/
/node_modules
.log
package-lock.json
diff --git a/README.md b/README.md
index c8f5371..677ef75 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-bulb.png b/icons/actions/mi-bulb.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-bulb.png
rename to icons/actions/mi-bulb.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-click.png b/icons/actions/mi-click.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-click.png
rename to icons/actions/mi-click.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-double-click.png b/icons/actions/mi-double-click.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-double-click.png
rename to icons/actions/mi-double-click.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-list.png b/icons/actions/mi-list.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-list.png
rename to icons/actions/mi-list.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-mute.png b/icons/actions/mi-mute.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-mute.png
rename to icons/actions/mi-mute.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-off.png b/icons/actions/mi-off.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-off.png
rename to icons/actions/mi-off.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-on.png b/icons/actions/mi-on.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-on.png
rename to icons/actions/mi-on.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-read.png b/icons/actions/mi-read.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-read.png
rename to icons/actions/mi-read.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-sound.png b/icons/actions/mi-sound.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-sound.png
rename to icons/actions/mi-sound.png
diff --git a/node-red-contrib-xiaomi-actions/icons/mi-toggle.png b/icons/actions/mi-toggle.png
similarity index 100%
rename from node-red-contrib-xiaomi-actions/icons/mi-toggle.png
rename to icons/actions/mi-toggle.png
diff --git a/icons/devices/door-icon.png b/icons/gateway-subdevices/door-icon.png
similarity index 100%
rename from icons/devices/door-icon.png
rename to icons/gateway-subdevices/door-icon.png
diff --git a/icons/devices/mi-all.png b/icons/gateway-subdevices/mi-all.png
similarity index 100%
rename from icons/devices/mi-all.png
rename to icons/gateway-subdevices/mi-all.png
diff --git a/icons/devices/mi-switch.png b/icons/gateway-subdevices/mi-switch.png
similarity index 100%
rename from icons/devices/mi-switch.png
rename to icons/gateway-subdevices/mi-switch.png
diff --git a/icons/devices/motion-icon.png b/icons/gateway-subdevices/motion-icon.png
similarity index 100%
rename from icons/devices/motion-icon.png
rename to icons/gateway-subdevices/motion-icon.png
diff --git a/icons/devices/outlet-icon.png b/icons/gateway-subdevices/outlet-icon.png
similarity index 100%
rename from icons/devices/outlet-icon.png
rename to icons/gateway-subdevices/outlet-icon.png
diff --git a/icons/devices/thermometer-icon.png b/icons/gateway-subdevices/thermometer-icon.png
similarity index 100%
rename from icons/devices/thermometer-icon.png
rename to icons/gateway-subdevices/thermometer-icon.png
diff --git a/icons/plug-wifi/outlet-wifi-icon.png b/icons/plug-wifi/outlet-wifi-icon.png
new file mode 100644
index 0000000..4da9524
Binary files /dev/null and b/icons/plug-wifi/outlet-wifi-icon.png differ
diff --git a/node-red-contrib-xiaomi-actions/xiaomi-actions.html b/node-red-contrib-xiaomi-actions/xiaomi-actions.html
index 01740a7..f535e44 100644
--- a/node-red-contrib-xiaomi-actions/xiaomi-actions.html
+++ b/node-red-contrib-xiaomi-actions/xiaomi-actions.html
@@ -1,179 +1,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/node-red-contrib-xiaomi-gateway/xiaomi-gateway.js b/node-red-contrib-xiaomi-gateway/xiaomi-gateway.js
deleted file mode 100644
index acc56b9..0000000
--- a/node-red-contrib-xiaomi-gateway/xiaomi-gateway.js
+++ /dev/null
@@ -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);
-}
diff --git a/node-red-contrib-xiaomi-ht/icons/thermometer-icon.png b/node-red-contrib-xiaomi-ht/icons/thermometer-icon.png
deleted file mode 100644
index b25be7d..0000000
Binary files a/node-red-contrib-xiaomi-ht/icons/thermometer-icon.png and /dev/null differ
diff --git a/node-red-contrib-xiaomi-ht/xiaomi-ht.html b/node-red-contrib-xiaomi-ht/xiaomi-ht.html
deleted file mode 100644
index 2bb277c..0000000
--- a/node-red-contrib-xiaomi-ht/xiaomi-ht.html
+++ /dev/null
@@ -1,122 +0,0 @@
-
-
-
-
-
diff --git a/node-red-contrib-xiaomi-ht/xiaomi-ht.js b/node-red-contrib-xiaomi-ht/xiaomi-ht.js
deleted file mode 100644
index 94914d1..0000000
--- a/node-red-contrib-xiaomi-ht/xiaomi-ht.js
+++ /dev/null
@@ -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);
-};
diff --git a/node-red-contrib-xiaomi-magnet/icons/door-icon.png b/node-red-contrib-xiaomi-magnet/icons/door-icon.png
deleted file mode 100644
index f33e892..0000000
Binary files a/node-red-contrib-xiaomi-magnet/icons/door-icon.png and /dev/null differ
diff --git a/node-red-contrib-xiaomi-magnet/xiaomi-magnet.html b/node-red-contrib-xiaomi-magnet/xiaomi-magnet.html
deleted file mode 100644
index 8d86a61..0000000
--- a/node-red-contrib-xiaomi-magnet/xiaomi-magnet.html
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
-
-
diff --git a/node-red-contrib-xiaomi-magnet/xiaomi-magnet.js b/node-red-contrib-xiaomi-magnet/xiaomi-magnet.js
deleted file mode 100644
index 93f6237..0000000
--- a/node-red-contrib-xiaomi-magnet/xiaomi-magnet.js
+++ /dev/null
@@ -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);
-};
diff --git a/node-red-contrib-xiaomi-motion/icons/motion-icon.png b/node-red-contrib-xiaomi-motion/icons/motion-icon.png
deleted file mode 100644
index 86c7e68..0000000
Binary files a/node-red-contrib-xiaomi-motion/icons/motion-icon.png and /dev/null differ
diff --git a/node-red-contrib-xiaomi-motion/xiaomi-motion.js b/node-red-contrib-xiaomi-motion/xiaomi-motion.js
deleted file mode 100644
index d88aedf..0000000
--- a/node-red-contrib-xiaomi-motion/xiaomi-motion.js
+++ /dev/null
@@ -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);
-};
diff --git a/node-red-contrib-xiaomi-switch/icons/mi-switch.png b/node-red-contrib-xiaomi-switch/icons/mi-switch.png
deleted file mode 100644
index 8ee44b2..0000000
Binary files a/node-red-contrib-xiaomi-switch/icons/mi-switch.png and /dev/null differ
diff --git a/node-red-contrib-xiaomi-switch/xiaomi-switch.js b/node-red-contrib-xiaomi-switch/xiaomi-switch.js
deleted file mode 100644
index 7360fe6..0000000
--- a/node-red-contrib-xiaomi-switch/xiaomi-switch.js
+++ /dev/null
@@ -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);
-};
diff --git a/package.json b/package.json
index d2dc66b..76e1774 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/devices/Gateway.ts b/src/devices/Gateway.ts
new file mode 100644
index 0000000..3f656c1
--- /dev/null
+++ b/src/devices/Gateway.ts
@@ -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()) {
+ ( 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
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/devices/GatewayMessage.ts b/src/devices/GatewayMessage.ts
new file mode 100644
index 0000000..8e2931e
--- /dev/null
+++ b/src/devices/GatewayMessage.ts
@@ -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";
+ }
+}
\ No newline at end of file
diff --git a/src/devices/GatewayMessageData.ts b/src/devices/GatewayMessageData.ts
new file mode 100644
index 0000000..81d6441
--- /dev/null
+++ b/src/devices/GatewayMessageData.ts
@@ -0,0 +1,20 @@
+export interface GatewayMessageHeartbeatData {
+ ip: string;
+}
+
+export interface GatewayMessageGetIdListData extends Array {
+}
+
+export interface GatewayMessageDefaultSubdeviceData {
+ voltage: number;
+}
+
+export interface GatewayMessageReadAckMagnetData extends GatewayMessageDefaultSubdeviceData {
+ status: string;
+}
+
+export interface GatewayMessageReadAckReportWeatherData extends GatewayMessageDefaultSubdeviceData {
+ temperature?: string;
+ humidity?: string;
+ pressure?: string;
+}
\ No newline at end of file
diff --git a/src/devices/GatewayServer.ts b/src/devices/GatewayServer.ts
new file mode 100644
index 0000000..8f02871
--- /dev/null
+++ b/src/devices/GatewayServer.ts
@@ -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( {
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/devices/GatewaySubdevice.ts b/src/devices/GatewaySubdevice.ts
new file mode 100644
index 0000000..ca3cd17
--- /dev/null
+++ b/src/devices/GatewaySubdevice.ts
@@ -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 = ( 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;
+ }
+}
\ No newline at end of file
diff --git a/src/devices/Magnet.ts b/src/devices/Magnet.ts
new file mode 100644
index 0000000..99f396a
--- /dev/null
+++ b/src/devices/Magnet.ts
@@ -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 = msg.data;
+ // mintime
+ if (this.status !== data.status) {
+ this.status = data.status;
+ this.emit('values-updated', this.sid);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/devices/Motion.ts b/src/devices/Motion.ts
new file mode 100644
index 0000000..305e55c
--- /dev/null
+++ b/src/devices/Motion.ts
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/devices/Switch.ts b/src/devices/Switch.ts
new file mode 100644
index 0000000..6580152
--- /dev/null
+++ b/src/devices/Switch.ts
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/devices/Weather.ts b/src/devices/Weather.ts
new file mode 100644
index 0000000..cc95369
--- /dev/null
+++ b/src/devices/Weather.ts
@@ -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 = msg.data;
+ ['temperature', 'humidity', 'pressure'].forEach((dataType: string) => {
+ if (data[dataType]) {
+ this[dataType] = parseInt(data[dataType]);
+ }
+ });
+ this.emit('values-updated', this.sid);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/devices/index.ts b/src/devices/index.ts
new file mode 100644
index 0000000..4c93553
--- /dev/null
+++ b/src/devices/index.ts
@@ -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';
\ No newline at end of file
diff --git a/src/nodes/actions/Action.ejs b/src/nodes/actions/Action.ejs
new file mode 100644
index 0000000..cbdbb3f
--- /dev/null
+++ b/src/nodes/actions/Action.ejs
@@ -0,0 +1,39 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/nodes/actions/GatewayLight.ejs b/src/nodes/actions/GatewayLight.ejs
new file mode 100644
index 0000000..02887ec
--- /dev/null
+++ b/src/nodes/actions/GatewayLight.ejs
@@ -0,0 +1,79 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/nodes/actions/GatewayLight.ts b/src/nodes/actions/GatewayLight.ts
new file mode 100644
index 0000000..27794fb
--- /dev/null
+++ b/src/nodes/actions/GatewayLight.ts
@@ -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( this, props);
+ ( this).setListeners();
+ }
+
+ protected setListeners() {
+ ( 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
+ };
+ }
+ ( this).send(msg);
+ });
+ }
+ }
+ RED.nodes.registerType(`${Constants.NODES_PREFIX}-actions gateway_light`, GatewayLight);
+};
\ No newline at end of file
diff --git a/src/nodes/actions/ReadAction.ts b/src/nodes/actions/ReadAction.ts
new file mode 100644
index 0000000..25cfb17
--- /dev/null
+++ b/src/nodes/actions/ReadAction.ts
@@ -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( this, props);
+
+ ( this).on('input', (msg) => {
+ if(msg.sid) {
+ msg.payload = {
+ cmd: ( this).type.replace(`${Constants.NODES_PREFIX}-actions `, ''),
+ sid: msg.sid
+ };
+ ( this).send(msg);
+ }
+ });
+ }
+ }
+
+ RED.nodes.registerType(`${Constants.NODES_PREFIX}-actions ${type}`, ReadAction);
+};
\ No newline at end of file
diff --git a/src/nodes/actions/WriteAction.ts b/src/nodes/actions/WriteAction.ts
new file mode 100644
index 0000000..2654adc
--- /dev/null
+++ b/src/nodes/actions/WriteAction.ts
@@ -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( this, props);
+
+ ( this).on('input', (msg) => {
+ if(msg.sid) {
+ msg.payload = {
+ cmd: "write",
+ data: {
+ status: ( this).type.replace(`${Constants.NODES_PREFIX}-actions `, ''),
+ sid: msg.sid
+ }
+ };
+ ( this).send(msg);
+ }
+ });
+ }
+ }
+ RED.nodes.registerType(`${Constants.NODES_PREFIX}-actions ${type}`, WriteAction);
+};
\ No newline at end of file
diff --git a/src/nodes/actions/index.ejs b/src/nodes/actions/index.ejs
new file mode 100644
index 0000000..0206057
--- /dev/null
+++ b/src/nodes/actions/index.ejs
@@ -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', {}) %>
\ No newline at end of file
diff --git a/src/nodes/actions/index.ts b/src/nodes/actions/index.ts
new file mode 100644
index 0000000..e578158
--- /dev/null
+++ b/src/nodes/actions/index.ts
@@ -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);
+ });
+};
\ No newline at end of file
diff --git a/src/nodes/constants.ts b/src/nodes/constants.ts
index 51b56bb..b335331 100644
--- a/src/nodes/constants.ts
+++ b/src/nodes/constants.ts
@@ -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;
+ };
}
\ No newline at end of file
diff --git a/src/nodes/gateway-subdevices/All.ejs b/src/nodes/gateway-subdevices/All.ejs
new file mode 100644
index 0000000..837b61a
--- /dev/null
+++ b/src/nodes/gateway-subdevices/All.ejs
@@ -0,0 +1,124 @@
+
+
+
+
+
diff --git a/src/nodes/gateway-subdevices/All.ts b/src/nodes/gateway-subdevices/All.ts
new file mode 100644
index 0000000..e3020c8
--- /dev/null
+++ b/src/nodes/gateway-subdevices/All.ts
@@ -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( this, props);
+ this.gateway = RED.nodes.getNode(( props).gateway);
+ this.onlyModels = All.getOnlyModelsValue(( props).onlyModels || []);
+ this.excludedSids = ( props).excludedSids;
+ }
+
+ protected setMessageListener() {
+ ( 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));
+ }
+ ( 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`, All);
+};
\ No newline at end of file
diff --git a/node-red-contrib-xiaomi-switch/xiaomi-switch.html b/src/nodes/gateway-subdevices/GatewaySubdevice.ejs
similarity index 74%
rename from node-red-contrib-xiaomi-switch/xiaomi-switch.html
rename to src/nodes/gateway-subdevices/GatewaySubdevice.ejs
index 97f79d9..bbb7d4f 100644
--- a/node-red-contrib-xiaomi-switch/xiaomi-switch.html
+++ b/src/nodes/gateway-subdevices/GatewaySubdevice.ejs
@@ -1,18 +1,18 @@
-
-
+ Sample payload after incoming incoming message:
+ <%- incomingSample %>
+ Where <%= (!!locals.incomingDetails)?incomingDetails + ',':'' %> batteryLevel is a computed percentage of remaining battery.
+
+
\ No newline at end of file
diff --git a/src/nodes/gateway-subdevices/GatewaySubdevice.ts b/src/nodes/gateway-subdevices/GatewaySubdevice.ts
new file mode 100644
index 0000000..62fd00a
--- /dev/null
+++ b/src/nodes/gateway-subdevices/GatewaySubdevice.ts
@@ -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( this, props);
+ this.gateway = RED.nodes.getNode(( props).gateway);
+
+ ( 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) {
+ 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";
+ }
+ ( this).status(status);
+ ( this).send([msg]);
+ }
+ }
+ // Prepare for request
+ else {
+ msg.sid = this.sid;
+ msg.gateway = this.gateway;
+ ( this).send(msg);
+ }
+ });
+ }
+ }
+ }
+
+ RED.nodes.registerType(`${Constants.NODES_PREFIX}-${type}`, GatewayDevice);
+};
\ No newline at end of file
diff --git a/node-red-contrib-xiaomi-motion/xiaomi-motion.html b/src/nodes/gateway-subdevices/Plug.ejs
similarity index 55%
rename from node-red-contrib-xiaomi-motion/xiaomi-motion.html
rename to src/nodes/gateway-subdevices/Plug.ejs
index 3721091..5d08fc4 100644
--- a/node-red-contrib-xiaomi-motion/xiaomi-motion.html
+++ b/src/nodes/gateway-subdevices/Plug.ejs
@@ -1,21 +1,21 @@
-
-
diff --git a/src/nodes/gateway-subdevices/Plug.ts b/src/nodes/gateway-subdevices/Plug.ts
new file mode 100644
index 0000000..93a4367
--- /dev/null
+++ b/src/nodes/gateway-subdevices/Plug.ts
@@ -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( this, props);
+ this.gateway = RED.nodes.getNode(( props).gateway);
+
+ ( this).status({fill:"grey", shape:"ring", text:"status"});
+ }
+
+ protected setListener() {
+ if (this.gateway) {
+ ( this).on('input', (msg) => {
+ var payload = msg.payload;
+ if(payload.sid) {
+ if (payload.sid == this.sid) {
+ if (payload.data.status && payload.data.status == "on") {
+ ( this).status({fill:"green", shape:"dot", text:"on"});
+ } else if (payload.data.status && payload.data.status == "off") {
+ ( this).status({fill:"red", shape:"dot", text:"off"});
+ }
+ ( this).send(msg);
+ }
+ }
+ // Prepare for request
+ else {
+ ( this).send(msg);
+ }
+ });
+ }
+ }
+ }
+
+ RED.nodes.registerType(`${Constants.NODES_PREFIX}-plug`, Plug);
+}
\ No newline at end of file
diff --git a/src/nodes/gateway-subdevices/index.ejs b/src/nodes/gateway-subdevices/index.ejs
new file mode 100644
index 0000000..73e206d
--- /dev/null
+++ b/src/nodes/gateway-subdevices/index.ejs
@@ -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: `status can be "open" or "close"`
+}) %>
+
+<%# ---------------------------------- 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: `humidy is in percents, pressure 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
+ }
+}`
+}) %>
\ No newline at end of file
diff --git a/src/nodes/gateway-subdevices/index.ts b/src/nodes/gateway-subdevices/index.ts
new file mode 100644
index 0000000..7d7441c
--- /dev/null
+++ b/src/nodes/gateway-subdevices/index.ts
@@ -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);
+ });
+};
\ No newline at end of file
diff --git a/src/nodes/gateway/GatewayConfigurator.ejs b/src/nodes/gateway/GatewayConfigurator.ejs
index 54ee938..74ab316 100644
--- a/src/nodes/gateway/GatewayConfigurator.ejs
+++ b/src/nodes/gateway/GatewayConfigurator.ejs
@@ -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('');
});
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 = $('', {class: "form-row"}).appendTo($('#input-subdevices'));
- $('', {value: device.sid, type: "hidden", name: "sid"}).appendTo(row);
- $('', {value: device.type, type: "hidden", name: "type"}).appendTo(row);
- $('', {for: "node-config-input-name-" + device.sid}).html('
' + device.sid).appendTo(row);
- $('', {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;
+ $('',{for:"node-config-input-sid-"+i, style:"margin-left: 3px; width: 15px;vertical-align:middle"}).appendTo(row);
+ var sid = $('',{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;
+ })
+ });
+ $('',{for:"node-config-input-desc-"+i, style:"margin-left: 7px; width: 20px;vertical-align:middle"}).html('').appendTo(row);
+ var desc = $('',{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 @@
-
-
-
-
-
-
+
+
- Note: use ip or sid - sid is better.
Devices
-
+
\ No newline at end of file
diff --git a/src/nodes/gateway/GatewayOut.ts b/src/nodes/gateway/GatewayOut.ts
index 1748987..85ef5f8 100644
--- a/src/nodes/gateway/GatewayOut.ts
+++ b/src/nodes/gateway/GatewayOut.ts
@@ -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( this, props);
- this.gatewayConf= RED.nodes.getNode(( props).gateway);
- ( this).status({fill:"red", shape:"ring", text: "offline"});
+ this.gatewayConf = RED.nodes.getNode(( props).gateway);
- this.setMessageListener();
+ (this).status({fill: "red", shape: "ring", text: "offline"});
+
+ if (this.gatewayConf.gateway) {
+ (this).status({fill: "blue", shape: "dot", text: "online"});
+ }
+
+ this.gatewayConf.on('gateway-online', () => {
+ (this).status({fill: "blue", shape: "dot", text: "online"});
+ });
+
+ this.gatewayConf.on('gateway-offline', () => {
+ (this).status({fill: "red", shape: "ring", text: "offline"});
+ });
}
protected setMessageListener() {
- ( this).on("input", (msg) => {
+ /*( 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);
- ( this).status({fill:"blue", shape:"dot", text: "online"});
-
- this.gateway.on('offline', () => {
- this.gateway = null;
- ( this).status({fill:"red", shape:"ring", text: "offline"});
- });
+ });*/
}
}
diff --git a/src/nodes/gateway/Searcher.ts b/src/nodes/gateway/Searcher.ts
deleted file mode 100644
index 7cde419..0000000
--- a/src/nodes/gateway/Searcher.ts
+++ /dev/null
@@ -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 = 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;
- }
-}
\ No newline at end of file
diff --git a/src/nodes/gateway/index.ts b/src/nodes/gateway/index.ts
index 3aaedb5..06000ef 100644
--- a/src/nodes/gateway/index.ts
+++ b/src/nodes/gateway/index.ts
@@ -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);
diff --git a/src/nodes/plug-wifi/index.ejs b/src/nodes/plug-wifi/index.ejs
new file mode 100644
index 0000000..3066cd1
--- /dev/null
+++ b/src/nodes/plug-wifi/index.ejs
@@ -0,0 +1,67 @@
+
+
+
+
+
diff --git a/src/nodes/plug-wifi/index.ts b/src/nodes/plug-wifi/index.ts
new file mode 100644
index 0000000..be0918b
--- /dev/null
+++ b/src/nodes/plug-wifi/index.ts
@@ -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);
+}
diff --git a/src/nodes/yeelight/YeelightConfigurator.ejs b/src/nodes/yeelight/YeelightConfigurator.ejs
index d395d3d..0b22dcb 100644
--- a/src/nodes/yeelight/YeelightConfigurator.ejs
+++ b/src/nodes/yeelight/YeelightConfigurator.ejs
@@ -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) {