From f4d54d714bb9f3fbd667d1b430316fdd36bcb89a Mon Sep 17 00:00:00 2001 From: Pierre CLEMENT Date: Sun, 18 Feb 2018 22:30:25 +0100 Subject: [PATCH] refactor(gateway): rewrite the gateway part --- .gitignore | 4 +- README.md | 13 +- .../icons => icons/actions}/mi-bulb.png | Bin .../icons => icons/actions}/mi-click.png | Bin .../actions}/mi-double-click.png | Bin .../icons => icons/actions}/mi-list.png | Bin .../icons => icons/actions}/mi-mute.png | Bin .../icons => icons/actions}/mi-off.png | Bin .../icons => icons/actions}/mi-on.png | Bin .../icons => icons/actions}/mi-read.png | Bin .../icons => icons/actions}/mi-sound.png | Bin .../icons => icons/actions}/mi-toggle.png | Bin .../door-icon.png | Bin .../mi-all.png | Bin .../mi-switch.png | Bin .../motion-icon.png | Bin .../outlet-icon.png | Bin .../thermometer-icon.png | Bin icons/plug-wifi/outlet-wifi-icon.png | Bin 0 -> 15858 bytes .../xiaomi-actions.html | 206 ++-------------- .../xiaomi-actions.js | 20 +- node-red-contrib-xiaomi-all/icons/mi-all.png | Bin 1050 -> 0 bytes .../icons/mijia-io.png | Bin 5990 -> 0 bytes .../icons/mijia.png | Bin 18401 -> 0 bytes .../xiaomi-gateway.html | 172 ------------- .../xiaomi-gateway.js | 229 ------------------ .../icons/thermometer-icon.png | Bin 3524 -> 0 bytes node-red-contrib-xiaomi-ht/xiaomi-ht.html | 122 ---------- node-red-contrib-xiaomi-ht/xiaomi-ht.js | 41 ---- .../icons/door-icon.png | Bin 4681 -> 0 bytes .../xiaomi-magnet.html | 120 --------- .../xiaomi-magnet.js | 9 - .../icons/motion-icon.png | Bin 5812 -> 0 bytes .../xiaomi-motion.js | 9 - .../icons/mi-switch.png | Bin 842 -> 0 bytes .../xiaomi-switch.js | 9 - package.json | 38 +-- src/devices/Gateway.ts | 90 +++++++ src/devices/GatewayMessage.ts | 58 +++++ src/devices/GatewayMessageData.ts | 20 ++ src/devices/GatewayServer.ts | 165 +++++++++++++ src/devices/GatewaySubdevice.ts | 45 ++++ src/devices/Magnet.ts | 39 +++ src/devices/Motion.ts | 17 ++ src/devices/Switch.ts | 17 ++ src/devices/Weather.ts | 49 ++++ src/devices/index.ts | 8 + src/nodes/actions/Action.ejs | 39 +++ src/nodes/actions/GatewayLight.ejs | 79 ++++++ src/nodes/actions/GatewayLight.ts | 34 +++ src/nodes/actions/ReadAction.ts | 22 ++ src/nodes/actions/WriteAction.ts | 24 ++ src/nodes/actions/index.ejs | 33 +++ src/nodes/actions/index.ts | 17 ++ src/nodes/constants.ts | 5 +- src/nodes/gateway-subdevices/All.ejs | 124 ++++++++++ src/nodes/gateway-subdevices/All.ts | 60 +++++ .../gateway-subdevices/GatewaySubdevice.ejs | 50 ++-- .../gateway-subdevices/GatewaySubdevice.ts | 49 ++++ .../nodes/gateway-subdevices/Plug.ejs | 82 ++++--- src/nodes/gateway-subdevices/Plug.ts | 40 +++ src/nodes/gateway-subdevices/index.ejs | 85 +++++++ src/nodes/gateway-subdevices/index.ts | 14 ++ src/nodes/gateway/GatewayConfigurator.ejs | 118 ++++++--- src/nodes/gateway/GatewayConfigurator.ts | 73 ++++-- src/nodes/gateway/GatewayIn.ts | 41 +++- src/nodes/gateway/GatewayOut.ejs | 27 +-- src/nodes/gateway/GatewayOut.ts | 41 ++-- src/nodes/gateway/Searcher.ts | 42 ---- src/nodes/gateway/index.ts | 4 +- src/nodes/plug-wifi/index.ejs | 67 +++++ src/nodes/plug-wifi/index.ts | 151 ++++++++++++ src/nodes/yeelight/YeelightConfigurator.ejs | 2 +- 73 files changed, 1652 insertions(+), 1171 deletions(-) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-bulb.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-click.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-double-click.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-list.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-mute.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-off.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-on.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-read.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-sound.png (100%) rename {node-red-contrib-xiaomi-actions/icons => icons/actions}/mi-toggle.png (100%) rename icons/{devices => gateway-subdevices}/door-icon.png (100%) rename icons/{devices => gateway-subdevices}/mi-all.png (100%) rename icons/{devices => gateway-subdevices}/mi-switch.png (100%) rename icons/{devices => gateway-subdevices}/motion-icon.png (100%) rename icons/{devices => gateway-subdevices}/outlet-icon.png (100%) rename icons/{devices => gateway-subdevices}/thermometer-icon.png (100%) create mode 100644 icons/plug-wifi/outlet-wifi-icon.png delete mode 100644 node-red-contrib-xiaomi-all/icons/mi-all.png delete mode 100644 node-red-contrib-xiaomi-gateway/icons/mijia-io.png delete mode 100644 node-red-contrib-xiaomi-gateway/icons/mijia.png delete mode 100644 node-red-contrib-xiaomi-gateway/xiaomi-gateway.html delete mode 100644 node-red-contrib-xiaomi-gateway/xiaomi-gateway.js delete mode 100644 node-red-contrib-xiaomi-ht/icons/thermometer-icon.png delete mode 100644 node-red-contrib-xiaomi-ht/xiaomi-ht.html delete mode 100644 node-red-contrib-xiaomi-ht/xiaomi-ht.js delete mode 100644 node-red-contrib-xiaomi-magnet/icons/door-icon.png delete mode 100644 node-red-contrib-xiaomi-magnet/xiaomi-magnet.html delete mode 100644 node-red-contrib-xiaomi-magnet/xiaomi-magnet.js delete mode 100644 node-red-contrib-xiaomi-motion/icons/motion-icon.png delete mode 100644 node-red-contrib-xiaomi-motion/xiaomi-motion.js delete mode 100644 node-red-contrib-xiaomi-switch/icons/mi-switch.png delete mode 100644 node-red-contrib-xiaomi-switch/xiaomi-switch.js create mode 100644 src/devices/Gateway.ts create mode 100644 src/devices/GatewayMessage.ts create mode 100644 src/devices/GatewayMessageData.ts create mode 100644 src/devices/GatewayServer.ts create mode 100644 src/devices/GatewaySubdevice.ts create mode 100644 src/devices/Magnet.ts create mode 100644 src/devices/Motion.ts create mode 100644 src/devices/Switch.ts create mode 100644 src/devices/Weather.ts create mode 100644 src/devices/index.ts create mode 100644 src/nodes/actions/Action.ejs create mode 100644 src/nodes/actions/GatewayLight.ejs create mode 100644 src/nodes/actions/GatewayLight.ts create mode 100644 src/nodes/actions/ReadAction.ts create mode 100644 src/nodes/actions/WriteAction.ts create mode 100644 src/nodes/actions/index.ejs create mode 100644 src/nodes/actions/index.ts create mode 100644 src/nodes/gateway-subdevices/All.ejs create mode 100644 src/nodes/gateway-subdevices/All.ts rename node-red-contrib-xiaomi-switch/xiaomi-switch.html => src/nodes/gateway-subdevices/GatewaySubdevice.ejs (74%) create mode 100644 src/nodes/gateway-subdevices/GatewaySubdevice.ts rename node-red-contrib-xiaomi-motion/xiaomi-motion.html => src/nodes/gateway-subdevices/Plug.ejs (55%) create mode 100644 src/nodes/gateway-subdevices/Plug.ts create mode 100644 src/nodes/gateway-subdevices/index.ejs create mode 100644 src/nodes/gateway-subdevices/index.ts delete mode 100644 src/nodes/gateway/Searcher.ts create mode 100644 src/nodes/plug-wifi/index.ejs create mode 100644 src/nodes/plug-wifi/index.ts 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 0000000000000000000000000000000000000000..4da95241f92af0adf6a027fed36e91ac26fef034 GIT binary patch literal 15858 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 b25be7d417a4af5fdf748586b375fc5b945a1c85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3524 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 Yb}8j?bVjen@Bc0Bi~5?y>bJuF3tIbei2wiq 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 f33e89298eab7ec1ff445cae51a08a906b552f9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4681 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 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 86c7e68cb92a3f4db84652d95b8deb7a51314fc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5812 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 { - // 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 8ee44b208eeffa9e1d09be5d6ffdd5330f06d668..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 842 zcmV-Q1GW5#P)g)R_nMo#_m-BYtb#_kjW#-=9 zJ-hRr^;R0Sa(t*!0V+TRsDMzT0#twsPyuFCfC^9nDnJFO02Po*r7Yk87gR2CT;OC) z0C!eu@StTRKz4w<7nlte5sQlq-ll1wqn`ovRXk#Ua08T5OMp|L3)SEn1sIB70A!1U zSPqGR{}~bm+@fHZlprau_`!f%1SCPOT9V{utHr>7%71~DRFiEKfEg^2-xC&d*`(M{ z-~o7CK^}qM;xLF%en1W`V;cQF+)2QZj}t*!g5pNtWZ~@J6kOsxLF@(1c`q;ta49a9 z9Rwdq-_p4M+gZBw*%aG=h-V$6xJHwJ$`n@z-2D?2Hxu-qvB~~-@cMz}!Iilv(w5T* zo>m>a^{iu3t^~-@Za|1>zV?9KW!!gIWsRD)J_;NQ!yaH35Q076q7bmdBj9Kl1Y8jUc7fTH zj|L~q0!sfX&{ zz2oz7C-^RttH3IQ0F#8Lg!s>wfIYO{s=)D3S>!HZQ{-ukE1m^Dde$+~j{xpqex9BN z;}qY6YFT_CIzntmg7Sg9m-%@g(2*C+@%sQN;S%TzI)N^ra{#&!oxn`+!!v+iKppQP zQ{FT65nA|Po?c?gJedKqpZ}Dvm3z2f0ZoL-{~*pAmm^{zk)`*65mEeI`IH&-t#DWH z29#>M1?tI~{0Q)#5Nm<8;1&1_5=G80@Bpj;78jq5FGDtH2|9yL6pIPorl#IQI@521 zHt!9k1ivrL+UIe8=?I)6$MzI%6cB1ufC^9nDj?LT02QDDR6wXv0V+TRsDMzD2IG{; U-^2PvtN;K207*qoM6N<$f>=g$Q~&?~ 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); - $('
-
- - -
- - + +
-

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) {