Compare commits
20 Commits
developmen
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 338419dca7 | |||
| 5183879654 | |||
| 524e691645 | |||
| 0fccbca7b2 | |||
| c24007e42c | |||
| 7698040f1c | |||
| e404c9a7ab | |||
|
|
4c8978decf | ||
|
|
627946c3d8 | ||
|
|
7f033a442b | ||
|
|
b984f415d5 | ||
|
|
aeb6fcd047 | ||
|
|
4e3c3055d7 | ||
|
|
ec82e3882c | ||
|
|
7880cc4f74 | ||
|
|
a07eef88ae | ||
|
|
af67f19217 | ||
|
|
e37c4aa71c | ||
|
|
600f8d859c | ||
|
|
6a9863814b |
36
.drone.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
kind: secret
|
||||
name: git_username
|
||||
get:
|
||||
path: secret/data/gitea
|
||||
name: api_access_user
|
||||
|
||||
---
|
||||
kind: secret
|
||||
name: git_password
|
||||
get:
|
||||
path: secret/data/gitea
|
||||
name: api_access_token
|
||||
|
||||
---
|
||||
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: build-and-publish
|
||||
image: node:16
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: git_password
|
||||
commands:
|
||||
- npm install
|
||||
- npm install typescript@latest
|
||||
- npm run build
|
||||
# Erstellt die Authentifizierung für die Registry (URL aus package.json)
|
||||
- echo "//git.familie-berner.de/api/packages/Public/npm/:_authToken=$GITEA_TOKEN" > .npmrc
|
||||
- npm publish
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
30
.gitignore
vendored
@@ -1,5 +1,29 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
# Dependencies
|
||||
/node_modules
|
||||
.log
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor / IDE
|
||||
*.iml
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Package manager files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
*.tgz
|
||||
|
||||
# Secrets
|
||||
.npmrc
|
||||
|
||||
18
.woodpecker/pipeline.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
steps:
|
||||
build-and-publish:
|
||||
image: node:16
|
||||
environment:
|
||||
# Woodpecker sucht nach einem Secret namens "GITEA_TOKEN"
|
||||
GITEA_TOKEN:
|
||||
from_secret: GITEA_TOKEN
|
||||
commands:
|
||||
- npm install
|
||||
- npm install typescript@latest
|
||||
- npm run build
|
||||
# Nutzt die Variable aus dem Environment
|
||||
- echo "//git.familie-berner.de/api/packages/Public/npm/:_authToken=$GITEA_TOKEN" > .npmrc
|
||||
- npm publish
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
event: push
|
||||
30
README.md
@@ -1,8 +1,7 @@
|
||||
# node-red-contrib-mi-devices
|
||||
|
||||
__:warning: I will not go further because I don't use node-red anymore and I don't have the time, but feel free to fork, PR & so on. If you want to maintain this module, feel free to ask.__
|
||||
|
||||
This module contains the following nodes to provide easy integration of the Xiaomi devices into node-red.
|
||||
This module is a fork of [Harald Rietman module, node-red-contrib-xiaomi-devices](https://github.com/hrietman/node-red-contrib-xiaomi-devices)
|
||||
|
||||
The following devices are currently supported:
|
||||
|
||||
@@ -13,11 +12,12 @@ The following devices are currently supported:
|
||||
* Button switch
|
||||
* Aqara smart wireless switch
|
||||
* Motion sensor
|
||||
* Aqara Motion sensor
|
||||
* Power plug (zigbee)
|
||||
* Power plug (wifi)
|
||||
* Yeelight White (mono)
|
||||
* Yeelight RGB (color)
|
||||
* Yeelight RGB
|
||||
|
||||
## Preparation
|
||||
## Preperation
|
||||
|
||||
To interact with the gateway, you need to enable the developer mode, aka LAN mode in the gateway (see below).
|
||||
|
||||
@@ -30,12 +30,6 @@ 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.
|
||||
@@ -59,6 +53,18 @@ Here an example of how to use the different nodes (screenshot of [importable flo
|
||||
Here are different flow (screenshot of [importable flows-sample.json](flows-sample.json?raw=true "Different flows using Mi Devices")):
|
||||

|
||||
|
||||
### Interpreting payload.msg
|
||||
|
||||
The following is an (incomplete) summary of interpreting payload.msg output from the mi-devices nodes
|
||||
#### payload.msg.cmd
|
||||
When utilising a gateway, device and debug node, you will see the following property of msg.payload.cmd along with a number of possible values:
|
||||
* cmd: 'read' - this is the result generated from a flow that utilises the 'read' node. (see 'outgoing' example above. The result of the 'xiaomi-ht' device connected to the 'read' node, will be output from the xiaomi-gateway incoming node, with a property of 'cmd' and value 'read'). Using a read node is the best way to obtain up to date values.
|
||||
* cmd: 'report' - is a result of a direct change in status of a device, for example the opening or closing of a magnet sensor, or a change in temperature.
|
||||
* cmd: 'heartbeat' - If the device is the gateway, then a heartbeat cmd will be sent every 10 seconds. If it is a sub-device, then plug-in devices (such as sockets and aircon helpers) will send a heartbeat every 10 minutes, other devices that sleep (e.g. zigbee devices), will send a heartbeat message every 60 minutes. If a device's current status is lost, a heartbeat message may be used to remedy the issue.
|
||||
* cmd 'write' - used to change the state of a Smart Socket
|
||||
|
||||
|
||||
|
||||
## Enable LAN mode
|
||||
|
||||
### Gateway
|
||||
@@ -95,9 +101,11 @@ The lightning icon should be underline un yellow.
|
||||
|
||||
## Sources
|
||||
|
||||
* [Harald Rietman node-red module](https://github.com/hrietman/node-red-contrib-xiaomi-devices)
|
||||
* [Domoticz Instructions](https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara))
|
||||
* [louisZl Gateway Local API](https://github.com/louisZL/lumi-gateway-local-api)
|
||||
* [Domoticz Gateway Code](https://github.com/domoticz/domoticz/blob/development/hardware/XiaomiGateway.cpp)
|
||||
* [Node-red UDP nodes](https://github.com/node-red/node-red/blob/master/nodes/core/io/32-udp.js)
|
||||
* [Yeelight specs](http://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf)
|
||||
|
||||
## Credits
|
||||
|
||||
113
dist/devices/gateway/Gateway.js
vendored
@@ -1,113 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const events = require("events");
|
||||
const crypto = require("crypto");
|
||||
const GatewayServer_1 = require("./GatewayServer");
|
||||
const _1 = require("./");
|
||||
const Color_1 = require("../../utils/Color");
|
||||
class Gateway extends events.EventEmitter {
|
||||
constructor(sid, ip) {
|
||||
super();
|
||||
this.sid = sid;
|
||||
this.ip = ip;
|
||||
this._subdevices = {};
|
||||
}
|
||||
set password(password) {
|
||||
this._password = password;
|
||||
}
|
||||
get key() {
|
||||
if (!this.lastToken || !this._password)
|
||||
return null;
|
||||
var cipher = crypto.createCipheriv('aes-128-cbc', this._password, Gateway.iv);
|
||||
var key = cipher.update(Buffer.from(this.lastToken), "ascii", "hex");
|
||||
cipher.final('hex');
|
||||
return key;
|
||||
}
|
||||
handleMessage(msg) {
|
||||
if (msg.data) {
|
||||
if (msg.model === "gateway" && msg.sid === this.sid && msg.token) {
|
||||
this.lastToken = msg.token;
|
||||
}
|
||||
}
|
||||
if (msg.isGetIdListAck()) {
|
||||
msg.data.forEach((sid) => {
|
||||
this.read(sid);
|
||||
});
|
||||
}
|
||||
if (msg.isReadAck() || msg.isReport()) {
|
||||
if (!this._subdevices[msg.sid]) {
|
||||
for (let SubDeviceClass of [_1.Magnet, _1.Motion, _1.Switch, _1.Weather]) {
|
||||
if (SubDeviceClass.acceptedModels.indexOf(msg.model) >= 0) {
|
||||
this._subdevices[msg.sid] = new SubDeviceClass(msg.sid, msg.model);
|
||||
this._subdevices[msg.sid].on('values-updated', (sidOrMessage) => {
|
||||
this.emit("subdevice-values-updated", sidOrMessage);
|
||||
});
|
||||
this.emit("subdevice-found", msg.sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this._subdevices[msg.sid]) {
|
||||
this._subdevices[msg.sid].handleMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
getSubdevice(sid) {
|
||||
return this._subdevices[sid] || null;
|
||||
}
|
||||
hasSubdevice(sid) {
|
||||
return !!this._subdevices[sid];
|
||||
}
|
||||
getIdList() {
|
||||
this.send({
|
||||
cmd: "get_id_list",
|
||||
});
|
||||
}
|
||||
read(sid) {
|
||||
this.send({ cmd: "read", sid: sid || this.sid });
|
||||
}
|
||||
setLight(brightness, rgb) {
|
||||
this.send({
|
||||
cmd: "write",
|
||||
data: {
|
||||
rgb: Color_1.Color.toValue(rgb.red, rgb.green, rgb.blue, brightness),
|
||||
sid: this.sid
|
||||
}
|
||||
});
|
||||
}
|
||||
playSound(musicId, volume) {
|
||||
this.send({
|
||||
cmd: "write",
|
||||
data: {
|
||||
mid: musicId,
|
||||
volume: volume,
|
||||
sid: this.sid
|
||||
}
|
||||
});
|
||||
}
|
||||
send(message) {
|
||||
let msg = Object.assign({}, message.payload || message);
|
||||
if (msg.cmd) {
|
||||
msg.sid = message.sid || this.sid;
|
||||
if (msg.gateway) {
|
||||
delete msg.gateway;
|
||||
}
|
||||
if (msg.cmd === "write") {
|
||||
msg.data.key = this.key;
|
||||
}
|
||||
GatewayServer_1.GatewayServer.getInstance().sendToGateway(this.sid, msg);
|
||||
}
|
||||
}
|
||||
get subdevices() {
|
||||
return this._subdevices;
|
||||
}
|
||||
toJSON() {
|
||||
return {
|
||||
sid: this.sid,
|
||||
ip: this.ip,
|
||||
key: this.password,
|
||||
subdevices: this.subdevices
|
||||
};
|
||||
}
|
||||
}
|
||||
Gateway.iv = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
|
||||
exports.Gateway = Gateway;
|
||||
30
dist/devices/gateway/GatewayMessage.js
vendored
@@ -1,30 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class GatewayMessage {
|
||||
constructor(raw) {
|
||||
Object.assign(this, raw);
|
||||
if (raw.port) {
|
||||
this.port = parseInt(raw.port);
|
||||
}
|
||||
if (raw.data) {
|
||||
this.data = JSON.parse(raw.data) || raw.data;
|
||||
}
|
||||
this.timestamp = +new Date;
|
||||
}
|
||||
isHeartbeat() {
|
||||
return this.cmd === "heartbeat";
|
||||
}
|
||||
isIam() {
|
||||
return this.cmd === "iam";
|
||||
}
|
||||
isGetIdListAck() {
|
||||
return this.cmd === "get_id_list_ack";
|
||||
}
|
||||
isReadAck() {
|
||||
return this.cmd === "read_ack";
|
||||
}
|
||||
isReport() {
|
||||
return this.cmd === "report";
|
||||
}
|
||||
}
|
||||
exports.GatewayMessage = GatewayMessage;
|
||||
2
dist/devices/gateway/GatewayMessageData.js
vendored
@@ -1,2 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
121
dist/devices/gateway/GatewayServer.js
vendored
@@ -1,121 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const events = require("events");
|
||||
const dgram = require("dgram");
|
||||
const _1 = require("./");
|
||||
const GatewayMessage_1 = require("./GatewayMessage");
|
||||
class GatewayServer extends events.EventEmitter {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this._gateways = {};
|
||||
this._gatewaysPing = {};
|
||||
}
|
||||
static getInstance() {
|
||||
if (!this.instance) {
|
||||
this.instance = new GatewayServer();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
discover(ipv = 4) {
|
||||
if (this.server) {
|
||||
return;
|
||||
}
|
||||
this.server = dgram.createSocket({
|
||||
type: `udp${ipv}`,
|
||||
reuseAddr: true
|
||||
});
|
||||
this.server.on('listening', () => {
|
||||
var address = this.server.address();
|
||||
//this.log(RED._("udp.status.listener-at",{host:address.address,port:address.port}));
|
||||
this.server.setBroadcast(true);
|
||||
try {
|
||||
this.server.setMulticastTTL(128);
|
||||
this.server.addMembership(GatewayServer.MULTICAST_ADDRESS, null);
|
||||
}
|
||||
catch (e) {
|
||||
/*if (e.errno == "EINVAL") {
|
||||
this.error(RED._("udp.errors.bad-mcaddress"));
|
||||
} else if (e.errno == "ENODEV") {
|
||||
this.error(RED._("udp.errors.interface"));
|
||||
} else {
|
||||
this.error(RED._("udp.errors.error",{error:e.errno}));
|
||||
}*/
|
||||
}
|
||||
});
|
||||
this.server.on("error", (err) => {
|
||||
/*if ((err.code == "EACCES") && (this.port < 1024)) {
|
||||
this.error(RED._("udp.errors.access-error"));
|
||||
} else {
|
||||
this.error(RED._("udp.errors.error",{error:err.code}));
|
||||
}*/
|
||||
this.server.close();
|
||||
delete this.server;
|
||||
});
|
||||
this.server.on('message', (message, remote) => {
|
||||
let msg = new GatewayMessage_1.GatewayMessage(JSON.parse(message.toString('utf8')));
|
||||
//console.log(msg);
|
||||
let gatewaySid = null;
|
||||
if ((msg.isHeartbeat() || msg.isIam()) && msg.model === "gateway") {
|
||||
if (!this._gateways[msg.sid]) {
|
||||
this._gateways[msg.sid] = new _1.Gateway(msg.sid, remote.address);
|
||||
this._gateways[msg.sid].getIdList();
|
||||
this.emit("gateway-online", msg.sid);
|
||||
}
|
||||
else {
|
||||
// Any IP update?
|
||||
this._gateways[msg.sid].ip = remote.address;
|
||||
}
|
||||
if (this._gatewaysPing[msg.sid]) {
|
||||
clearTimeout(this._gatewaysPing[msg.sid]);
|
||||
delete this._gatewaysPing[msg.sid];
|
||||
}
|
||||
// Consider the gateway as unreachable after 2 heartbeats missed (1 heartbeat every 10s)
|
||||
this._gatewaysPing[msg.sid] = setTimeout(() => {
|
||||
this.emit("gateway-offline", msg.sid);
|
||||
delete this._gateways[msg.sid];
|
||||
}, 25 * 1000);
|
||||
gatewaySid = msg.sid;
|
||||
}
|
||||
if (!gatewaySid) {
|
||||
gatewaySid = Object.keys(this._gateways).filter((gatewaySid) => this._gateways[gatewaySid].ip === remote.address)[0];
|
||||
}
|
||||
gatewaySid && this._gateways[gatewaySid] && this._gateways[gatewaySid].handleMessage(msg);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.server.bind(GatewayServer.SERVER_PORT, null);
|
||||
let msg = Buffer.from(JSON.stringify({ cmd: "whois" }));
|
||||
this.server.send(msg, 0, msg.length, GatewayServer.MULTICAST_PORT, GatewayServer.MULTICAST_ADDRESS);
|
||||
resolve(this.server);
|
||||
}
|
||||
catch (e) {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
stop() {
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
delete this.server;
|
||||
}
|
||||
}
|
||||
getGateway(sid) {
|
||||
return this._gateways[sid] || null;
|
||||
}
|
||||
hasGateway(sid) {
|
||||
return !!this._gateways[sid];
|
||||
}
|
||||
get gateways() {
|
||||
return this._gateways;
|
||||
}
|
||||
sendToGateway(sid, message) {
|
||||
if (this.server && this._gateways[sid]) {
|
||||
let msg = Buffer.from(JSON.stringify(message));
|
||||
this.server.send(msg, 0, msg.length, GatewayServer.SERVER_PORT, this._gateways[sid].ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
GatewayServer.MULTICAST_ADDRESS = '224.0.0.50';
|
||||
GatewayServer.MULTICAST_PORT = 4321;
|
||||
GatewayServer.SERVER_PORT = 9898;
|
||||
exports.GatewayServer = GatewayServer;
|
||||
40
dist/devices/gateway/GatewaySubdevice.js
vendored
@@ -1,40 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const events = require("events");
|
||||
class GatewaySubdevice extends events.EventEmitter {
|
||||
constructor(sid, model) {
|
||||
super();
|
||||
this.sid = sid;
|
||||
this.model = model;
|
||||
}
|
||||
get batteryLevel() {
|
||||
/*
|
||||
When full, CR2032 batteries are between 3 and 3.4V
|
||||
http://farnell.com/datasheets/1496885.pdf
|
||||
*/
|
||||
return this.voltage ? Math.min(Math.round((this.voltage - 2200) / 10), 100) : -1;
|
||||
}
|
||||
handleMessage(msg) {
|
||||
if (msg.data.voltage) {
|
||||
this.voltage = msg.data.voltage;
|
||||
}
|
||||
this.message = msg;
|
||||
}
|
||||
static get acceptedModels() {
|
||||
return [];
|
||||
}
|
||||
;
|
||||
toJSON() {
|
||||
let json = {};
|
||||
for (let prop of Object.keys(this)) {
|
||||
json[prop] = this[prop];
|
||||
}
|
||||
delete json._events;
|
||||
delete json._eventsCount;
|
||||
delete json._maxListeners;
|
||||
json.batteryLevel = this.batteryLevel;
|
||||
json.internalModel = this.internalModel;
|
||||
return json;
|
||||
}
|
||||
}
|
||||
exports.GatewaySubdevice = GatewaySubdevice;
|
||||
32
dist/devices/gateway/Magnet.js
vendored
@@ -1,32 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const GatewaySubdevice_1 = require("./GatewaySubdevice");
|
||||
class Magnet extends GatewaySubdevice_1.GatewaySubdevice {
|
||||
static get acceptedModels() {
|
||||
return ['magnet', 'sensor_magnet.aq2'];
|
||||
}
|
||||
get internalModel() {
|
||||
return 'mi.magnet';
|
||||
}
|
||||
isClosed() {
|
||||
return this.status === "close";
|
||||
}
|
||||
isOpened() {
|
||||
return this.status === "open";
|
||||
}
|
||||
isUnkownState() {
|
||||
return this.status === "unkown";
|
||||
}
|
||||
handleMessage(msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.isReadAck() || msg.isReport()) {
|
||||
let data = msg.data;
|
||||
// TODO: mintime
|
||||
if (this.status !== data.status) {
|
||||
this.status = data.status;
|
||||
this.emit('values-updated', this.sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.Magnet = Magnet;
|
||||
29
dist/devices/gateway/Motion.js
vendored
@@ -1,29 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const GatewaySubdevice_1 = require("./GatewaySubdevice");
|
||||
class Motion extends GatewaySubdevice_1.GatewaySubdevice {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.lux = 0;
|
||||
}
|
||||
static get acceptedModels() {
|
||||
return ['motion', 'sensor_motion.aq2'];
|
||||
}
|
||||
get internalModel() {
|
||||
return 'mi.motion';
|
||||
}
|
||||
handleMessage(msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.isReadAck() || msg.isReport()) {
|
||||
let data = msg.data;
|
||||
if (data.lux) {
|
||||
this.lux = parseInt(data.lux);
|
||||
}
|
||||
if (data.status === "motion") {
|
||||
this.lastMotionTimestamp = data.timestamp;
|
||||
this.emit('values-updated', { sid: this.sid, data: { hasMotion: true } });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.Motion = Motion;
|
||||
15
dist/devices/gateway/Switch.js
vendored
@@ -1,15 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const GatewaySubdevice_1 = require("./GatewaySubdevice");
|
||||
class Switch extends GatewaySubdevice_1.GatewaySubdevice {
|
||||
static get acceptedModels() {
|
||||
return ['switch', 'sensor_switch.aq2'];
|
||||
}
|
||||
get internalModel() {
|
||||
return 'mi.switch';
|
||||
}
|
||||
handleMessage(msg) {
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
exports.Switch = Switch;
|
||||
36
dist/devices/gateway/Weather.js
vendored
@@ -1,36 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const GatewaySubdevice_1 = require("./GatewaySubdevice");
|
||||
class Weather extends GatewaySubdevice_1.GatewaySubdevice {
|
||||
static get acceptedModels() {
|
||||
return ['sensor_ht', 'weather.v1'];
|
||||
}
|
||||
get internalModel() {
|
||||
return 'mi.weather';
|
||||
}
|
||||
get temperatureInDegrees() {
|
||||
return this.temperature / 100;
|
||||
}
|
||||
get humidityInPercent() {
|
||||
return this.humidity / 100;
|
||||
}
|
||||
get pressureInBar() {
|
||||
return this.pressure / 100000;
|
||||
}
|
||||
get pressureInhPa() {
|
||||
return this.pressure / 100;
|
||||
}
|
||||
handleMessage(msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.isReadAck() || msg.isReport()) {
|
||||
let data = msg.data;
|
||||
['temperature', 'humidity', 'pressure'].forEach((dataType) => {
|
||||
if (data[dataType]) {
|
||||
this[dataType] = parseInt(data[dataType]);
|
||||
}
|
||||
});
|
||||
this.emit('values-updated', this.sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.Weather = Weather;
|
||||
14
dist/devices/gateway/index.js
vendored
@@ -1,14 +0,0 @@
|
||||
"use strict";
|
||||
function __export(m) {
|
||||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__export(require("./"));
|
||||
__export(require("./Gateway"));
|
||||
__export(require("./GatewayMessage"));
|
||||
__export(require("./GatewayServer"));
|
||||
__export(require("./GatewaySubdevice"));
|
||||
__export(require("./Magnet"));
|
||||
__export(require("./Motion"));
|
||||
__export(require("./Switch"));
|
||||
__export(require("./Weather"));
|
||||
45
dist/devices/yeelight/YeelightServer.js
vendored
@@ -1,45 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const events = require("events");
|
||||
const YeelightSearch = require("yeelight-wifi");
|
||||
class YeelightServer extends events.EventEmitter {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this._bulbs = {};
|
||||
this._bulbsJson = {};
|
||||
}
|
||||
static getInstance() {
|
||||
if (!this.instance) {
|
||||
this.instance = new YeelightServer();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
get bulbs() {
|
||||
return this._bulbsJson;
|
||||
}
|
||||
getBulb(sid) {
|
||||
return this._bulbs[sid];
|
||||
}
|
||||
discover() {
|
||||
new Promise(() => {
|
||||
(new YeelightSearch()).on('found', (bulb) => {
|
||||
bulb.sid = parseInt(bulb.id);
|
||||
if (!this._bulbs[bulb.sid]) {
|
||||
this._bulbs[bulb.sid] = bulb;
|
||||
this._bulbsJson[bulb.sid] = YeelightServer.bulbToJSON(bulb);
|
||||
this.emit("yeelight-online", bulb.sid);
|
||||
}
|
||||
});
|
||||
});
|
||||
// TODO: disconected ?
|
||||
}
|
||||
static bulbToJSON(bulb) {
|
||||
return {
|
||||
sid: bulb.sid,
|
||||
ip: bulb.hostname,
|
||||
name: bulb.name,
|
||||
model: bulb.model
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.YeelightServer = YeelightServer;
|
||||
0
dist/devices/yeelight/index.js
vendored
26
dist/nodes/actions/GatewayPlaySound.js
vendored
@@ -1,26 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED) => {
|
||||
class GatewayPlaySound {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.mid = parseInt(props.mid);
|
||||
this.volume = parseInt(props.volume);
|
||||
this.setListeners();
|
||||
}
|
||||
setListeners() {
|
||||
this.on('input', (msg) => {
|
||||
if (msg.sid) {
|
||||
msg.payload = {
|
||||
action: "playSound",
|
||||
mid: msg.mid || this.mid,
|
||||
volume: msg.volume || this.volume
|
||||
};
|
||||
}
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions gateway_play_sound`, GatewayPlaySound);
|
||||
};
|
||||
23
dist/nodes/actions/GatewayStopSound.js
vendored
@@ -1,23 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED) => {
|
||||
class GatewayStopSound {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.setListeners();
|
||||
}
|
||||
setListeners() {
|
||||
this.on('input', (msg) => {
|
||||
if (msg.sid) {
|
||||
msg.payload = {
|
||||
action: "playSound",
|
||||
mid: 1000
|
||||
};
|
||||
}
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions gateway_stop_sound`, GatewayStopSound);
|
||||
};
|
||||
24
dist/nodes/actions/Light.js
vendored
@@ -1,24 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED) => {
|
||||
class Light {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.color = props.color;
|
||||
this.brightness = props.brightness;
|
||||
this.setListeners();
|
||||
}
|
||||
setListeners() {
|
||||
this.on('input', (msg) => {
|
||||
msg.payload = {
|
||||
action: "setLight",
|
||||
color: msg.color || this.color,
|
||||
brightness: msg.brightness || this.brightness
|
||||
};
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions light`, Light);
|
||||
};
|
||||
20
dist/nodes/actions/ReadAction.js
vendored
@@ -1,20 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED, type) => {
|
||||
class ReadAction {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.on('input', (msg) => {
|
||||
if (msg.sid) {
|
||||
msg.payload = {
|
||||
cmd: this.type.replace(`${constants_1.Constants.NODES_PREFIX}-actions `, ''),
|
||||
sid: msg.sid
|
||||
};
|
||||
this.send(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions ${type}`, ReadAction);
|
||||
};
|
||||
18
dist/nodes/actions/ToggleAction.js
vendored
@@ -1,18 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED, action) => {
|
||||
class ToggleAction {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.setListeners();
|
||||
}
|
||||
setListeners() {
|
||||
this.on('input', (msg) => {
|
||||
msg.payload = { action };
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions ${action}`, ToggleAction);
|
||||
};
|
||||
23
dist/nodes/actions/WriteAction.js
vendored
@@ -1,23 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED, type) => {
|
||||
class WriteAction {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.on('input', (msg) => {
|
||||
if (msg.sid) {
|
||||
msg.payload = {
|
||||
cmd: "write",
|
||||
data: {
|
||||
status: this.type.replace(`${constants_1.Constants.NODES_PREFIX}-actions `, ''),
|
||||
sid: msg.sid
|
||||
}
|
||||
};
|
||||
this.send(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions ${type}`, WriteAction);
|
||||
};
|
||||
21
dist/nodes/actions/index.js
vendored
@@ -1,21 +0,0 @@
|
||||
"use strict";
|
||||
const ReadAction_1 = require("./ReadAction");
|
||||
const WriteAction_1 = require("./WriteAction");
|
||||
const Light_1 = require("./Light");
|
||||
const GatewayPlaySound_1 = require("./GatewayPlaySound");
|
||||
const GatewayStopSound_1 = require("./GatewayStopSound");
|
||||
const ToggleAction_1 = require("./ToggleAction");
|
||||
module.exports = (RED) => {
|
||||
["read", "get_id_list"].forEach((action) => {
|
||||
ReadAction_1.default(RED, action);
|
||||
});
|
||||
["click", "double_click"].forEach((action) => {
|
||||
WriteAction_1.default(RED, action);
|
||||
});
|
||||
Light_1.default(RED);
|
||||
GatewayPlaySound_1.default(RED);
|
||||
GatewayStopSound_1.default(RED);
|
||||
["turn_on", "turn_off", "toggle"].forEach(action => {
|
||||
ToggleAction_1.default(RED, action);
|
||||
});
|
||||
};
|
||||
10
dist/nodes/constants.js
vendored
@@ -1,10 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class Constants {
|
||||
static get NODES_PREFIX() {
|
||||
let packageJson = require(`${__dirname}/../../package`);
|
||||
return packageJson.config.nodes_prefix;
|
||||
}
|
||||
;
|
||||
}
|
||||
exports.Constants = Constants;
|
||||
67
dist/nodes/gateway-subdevices/All.js
vendored
@@ -1,67 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
const uniqid = require("uniqid");
|
||||
exports.default = (RED) => {
|
||||
class All {
|
||||
static getOnlyModelsValue(input) {
|
||||
var cleanOnlyModels = [];
|
||||
input.forEach((value) => {
|
||||
cleanOnlyModels = cleanOnlyModels.concat(value.split(','));
|
||||
});
|
||||
return cleanOnlyModels;
|
||||
}
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.gatewayConf = RED.nodes.getNode(props.gateway);
|
||||
this.onlyModels = All.getOnlyModelsValue(props.onlyModels || []);
|
||||
this.excludedSids = props.excludedSids;
|
||||
this.setMessageListener();
|
||||
}
|
||||
setMessageListener() {
|
||||
this.on('input', (msg) => {
|
||||
if (this.gatewayConf) {
|
||||
// Filter input
|
||||
if (msg.payload && msg.payload.model && msg.payload.sid) {
|
||||
if (!this.isDeviceValid(msg.payload.sid)) {
|
||||
msg = null;
|
||||
}
|
||||
this.send(msg);
|
||||
}
|
||||
else {
|
||||
let partsId = uniqid();
|
||||
Object.keys(this.gatewayConf.deviceList || {})
|
||||
.filter((sid) => this.isDeviceValid(sid))
|
||||
.forEach((sid, i, subSids) => {
|
||||
let curMsg = Object.assign({}, msg);
|
||||
delete curMsg._msgid;
|
||||
curMsg.parts = {
|
||||
id: partsId,
|
||||
index: i,
|
||||
count: subSids.length,
|
||||
};
|
||||
curMsg.sid = sid;
|
||||
curMsg.gateway = this.gatewayConf;
|
||||
this.send(curMsg);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
isDeviceValid(sid) {
|
||||
if ((!this.onlyModels || this.onlyModels.length == 0) && (!this.excludedSids || this.excludedSids.length == 0)) {
|
||||
return true;
|
||||
}
|
||||
let device = this.gatewayConf.deviceList[sid];
|
||||
// Is excluded
|
||||
if ((this.excludedSids && this.excludedSids.length != 0) && this.excludedSids.indexOf(sid) >= 0) {
|
||||
return false;
|
||||
}
|
||||
if ((this.onlyModels && this.onlyModels.length != 0) && this.onlyModels.indexOf(device.internalModel) >= 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-all`, All);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED, type) => {
|
||||
class GatewayDevice {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.gateway = RED.nodes.getNode(props.gateway);
|
||||
this.sid = props.sid;
|
||||
this.status({ fill: "grey", shape: "ring", text: "battery - na" });
|
||||
this.setMessageListener();
|
||||
}
|
||||
setMessageListener() {
|
||||
if (this.gateway) {
|
||||
this.on('input', (msg) => {
|
||||
let payload = msg.payload;
|
||||
// Input from gateway
|
||||
if (payload.sid) {
|
||||
if (payload.sid == this.sid) {
|
||||
let batteryLevel = payload.batteryLevel;
|
||||
var status = {
|
||||
fill: "green", shape: "dot",
|
||||
text: "battery - " + batteryLevel + "%"
|
||||
};
|
||||
if (batteryLevel < 10) {
|
||||
status.fill = "red";
|
||||
}
|
||||
else if (batteryLevel < 45) {
|
||||
status.fill = "yellow";
|
||||
}
|
||||
this.status(status);
|
||||
this.send([msg]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg.sid = this.sid;
|
||||
msg.gateway = this.gateway;
|
||||
this.send(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-mi.${type}`, GatewayDevice);
|
||||
};
|
||||
34
dist/nodes/gateway-subdevices/Plug.js
vendored
@@ -1,34 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED) => {
|
||||
class Plug {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.gateway = RED.nodes.getNode(props.gateway);
|
||||
this.status({ fill: "grey", shape: "ring", text: "status" });
|
||||
}
|
||||
setListener() {
|
||||
if (this.gateway) {
|
||||
this.on('input', (msg) => {
|
||||
var payload = msg.payload;
|
||||
if (payload.sid) {
|
||||
if (payload.sid == this.sid) {
|
||||
if (payload.data.status && payload.data.status == "on") {
|
||||
this.status({ fill: "green", shape: "dot", text: "on" });
|
||||
}
|
||||
else if (payload.data.status && payload.data.status == "off") {
|
||||
this.status({ fill: "red", shape: "dot", text: "off" });
|
||||
}
|
||||
this.send(msg);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.send(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-plug`, Plug);
|
||||
};
|
||||
729
dist/nodes/gateway-subdevices/index.html
vendored
@@ -1,729 +0,0 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-all', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"mi-devices-gateway configurator"},
|
||||
name: {value: ""},
|
||||
onlyModels: {value: []},
|
||||
excludedSids: { value: []}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
outputLabels: ["All devices"],
|
||||
paletteLabel: "all",
|
||||
icon: "mi-all.png",
|
||||
label: function () {
|
||||
return this.name || "xiaomi-all";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
function getOnlyModelsValue(input) {
|
||||
var cleanOnlyModels = [];
|
||||
input.forEach(function(value) {
|
||||
cleanOnlyModels = cleanOnlyModels.concat(value.split(','));
|
||||
});
|
||||
return cleanOnlyModels;
|
||||
}
|
||||
|
||||
function changeGateway(gateway, onlyModels, excludedSids) {
|
||||
var configNodeID = gateway || $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
onlyModels = getOnlyModelsValue(onlyModels || $('#node-input-onlyModels').val() || []);
|
||||
excludedSids = excludedSids || $('#node-input-excludedSids').val() || [];
|
||||
$('#node-input-excludedSids').empty();
|
||||
for (sid in configNode.deviceList) {
|
||||
var device = configNode.deviceList[sid];
|
||||
if (onlyModels.length == 0 || onlyModels.indexOf(device.internalModel) >= 0) {
|
||||
var option = $('<option value="' + sid + '">' + device.name + '</option>');
|
||||
if(excludedSids && excludedSids.indexOf(sid) >= 0) {
|
||||
option.prop('selected', true);
|
||||
}
|
||||
$('#node-input-excludedSids').append(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeGateway(this.gateway, this.onlyModels, this.excludedSids);
|
||||
$("#node-input-gateway, #node-input-onlyModels").change(function () {
|
||||
changeGateway();
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
if(!$('#node-input-onlyModels').val()) {
|
||||
this.onlyModels = [];
|
||||
}
|
||||
if(!$('#node-input-excludedSids').val()) {
|
||||
this.excludedSids = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-all">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<hr />
|
||||
<h5>Filters</h5>
|
||||
<div class="form-row">
|
||||
<label for="node-input-onlyModels"><i class="icon-tag"></i> Only</label>
|
||||
<select multiple id="node-input-onlyModels">
|
||||
<option value="mi.weather">Temperature/humidty</option>
|
||||
<option value="mi.motion">Motion</option>
|
||||
<option value="mi.switch">Switches</option>
|
||||
<option value="mi.magnet">Magnets</option>
|
||||
<option value="mi.plug">Plugs</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-excludedSids"><i class="icon-tag"></i> Exclude</label>
|
||||
<select multiple id="node-input-excludedSids" size=10></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-all">
|
||||
<p>All devices registred in the gateway, except gateway itself.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>When use as an incoming filter node, <code>sid</code> and <code>model</code> are mandatory.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<li>Devices output
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">array</span></dt>
|
||||
<dd>Array of devices.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>Sample payload:</p>
|
||||
<p><pre>[
|
||||
{sid: "128d0901db1fa8", desc: "Door sensor" model: "magnet"},
|
||||
{sid: "151d0401ab2491", desc: "Heat sensor", model: "sensor_ht"},
|
||||
{sid: "658d030171427c", desc: "Button", model: "switch"}
|
||||
]</pre>
|
||||
</p>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-plug', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"mi-devices-gateway configurator"},
|
||||
name: {value: ""},
|
||||
sid: {value: "", required: true},
|
||||
onmsg: {value: ""},
|
||||
offmsg: {value: ""},
|
||||
output: {value: "0"}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
paletteLabel: "plug (zigbee)",
|
||||
icon: "outlet-icon.png",
|
||||
label: function () {
|
||||
return this.name || "xiaomi-plug";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
if(node.sid) {
|
||||
$('#node-input-sid').val(node.sid);
|
||||
}
|
||||
|
||||
function changeGateway(model) {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
$('#node-input-sid').empty();
|
||||
for (key in configNode.deviceList) {
|
||||
var device = configNode.deviceList[key];
|
||||
if (device.model === model) {
|
||||
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + '</option>');
|
||||
}
|
||||
}
|
||||
if(node.sid) {
|
||||
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-sid").change(function () {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val($('#node-input-sid option:selected').text());
|
||||
}
|
||||
});
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway("plug");
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
node.sid = $("#node-input-sid").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-plug">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
|
||||
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-plug">
|
||||
<p>The Xiaomi plug (zigbee) node</p>
|
||||
|
||||
<p>This is the plug (socket) version which is attached to a Xiaomi gateway. The Wifi version is not yet supported.</p>
|
||||
<p>To switch an output you need to specify the key of the gateway in the gateway configuration; without the key
|
||||
no output can be switched. To retrieve the gateway key consult the Xiaomi Mi Home App.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">string | json</span>
|
||||
</dt>
|
||||
<dd>When the node is used as filter, gateway <code>plug</code> message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>. Or <code>on</code> or <code>off</code>.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<li>Status output
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">string | json</span></dt>
|
||||
<dd>raw data, value or template.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>Control output
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">json</span></dt>
|
||||
<dd>Gateway <code>write_cmd</code> to switch the output on or off.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming json message is parsed if the type model is <code>plug</code> and
|
||||
the <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>On the input you can send the string <code>on</code> to switch the plug on. To turn it off just send the string <code>off</code></p>
|
||||
<p>Sample message:</p>
|
||||
<p><pre>{
|
||||
cmd: "write_ack"
|
||||
model: "plug"
|
||||
sid: "158d00012f1fb5"
|
||||
short_id: 47414
|
||||
data: {
|
||||
voltage:3600,
|
||||
status:"off",
|
||||
inuse:"0",
|
||||
power_consumed:"4000",
|
||||
load_power:"0"
|
||||
}
|
||||
}</pre></p>
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-mi.magnet', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"mi-devices-gateway configurator"},
|
||||
name: {value: ""},
|
||||
sid: {value: "", required: true}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
paletteLabel: "magnet",
|
||||
icon: "door-icon.png",
|
||||
label: function () {
|
||||
return this.name || "mi-devices mi.magnet";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
if(node.sid) {
|
||||
$('#node-input-sid').val(node.sid);
|
||||
}
|
||||
|
||||
function changeGateway(model) {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
$('#node-input-sid').empty();
|
||||
for (sid in configNode.deviceList) {
|
||||
var device = configNode.deviceList[sid];
|
||||
if (device.internalModel === model) {
|
||||
$('#node-input-sid').append('<option value="' + sid + '">' + device.name + ' - ' + sid + '</option>');
|
||||
}
|
||||
}
|
||||
if(node.sid) {
|
||||
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-sid").change(function () {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val($('#node-input-sid option:selected').text());
|
||||
}
|
||||
});
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway("mi.magnet");
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
node.sid = $("#node-input-sid").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-mi.magnet">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
|
||||
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-mi.magnet">
|
||||
<p>The Xiaomi contact sensor node</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>
|
||||
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
|
||||
<hr>
|
||||
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">object</span></dt>
|
||||
<dd>Data from gateway when used as a filter (see below).</dd>
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device SID.</dd>
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the device is registred.</dd>
|
||||
</dl>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>Sample payload after incoming incoming message:</p>
|
||||
<p><pre>{
|
||||
cmd: "read_ack"
|
||||
model: "magnet"
|
||||
sid: "158d000112fb5d"
|
||||
short_id: 50301
|
||||
data: {
|
||||
voltage: 3015,
|
||||
status: "close",
|
||||
batteryLevel: 23
|
||||
}
|
||||
}</pre>
|
||||
Where <code>status</code> can be <code>"open"</code> or <code>"close"</code>, <code>batteryLevel</code> is a computed percentage of remaining battery.
|
||||
</p>
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-mi.motion', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"mi-devices-gateway configurator"},
|
||||
name: {value: ""},
|
||||
sid: {value: "", required: true}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
paletteLabel: "motion",
|
||||
icon: "motion-icon.png",
|
||||
label: function () {
|
||||
return this.name || "mi-devices mi.motion";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
if(node.sid) {
|
||||
$('#node-input-sid').val(node.sid);
|
||||
}
|
||||
|
||||
function changeGateway(model) {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
$('#node-input-sid').empty();
|
||||
for (sid in configNode.deviceList) {
|
||||
var device = configNode.deviceList[sid];
|
||||
if (device.internalModel === model) {
|
||||
$('#node-input-sid').append('<option value="' + sid + '">' + device.name + ' - ' + sid + '</option>');
|
||||
}
|
||||
}
|
||||
if(node.sid) {
|
||||
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-sid").change(function () {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val($('#node-input-sid option:selected').text());
|
||||
}
|
||||
});
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway("mi.motion");
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
node.sid = $("#node-input-sid").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-mi.motion">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
|
||||
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-mi.motion">
|
||||
<p>The Xiaomi body motion sensor node</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>
|
||||
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
|
||||
<hr>
|
||||
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">object</span></dt>
|
||||
<dd>Data from gateway when used as a filter (see below).</dd>
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device SID.</dd>
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the device is registred.</dd>
|
||||
</dl>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>Sample payload after incoming incoming message:</p>
|
||||
<p><pre>{
|
||||
cmd: "read_ack"
|
||||
model: "motion"
|
||||
sid: "158d00015ef56c"
|
||||
short_id: 21672
|
||||
data: {
|
||||
voltage: 3035,
|
||||
status: "motion",
|
||||
batteryLevel: 45
|
||||
}
|
||||
}</pre>
|
||||
Where <code>batteryLevel</code> is a computed percentage of remaining battery.
|
||||
</p>
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-mi.weather', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"mi-devices-gateway configurator"},
|
||||
name: {value: ""},
|
||||
sid: {value: "", required: true}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
paletteLabel: "weather",
|
||||
icon: "thermometer-icon.png",
|
||||
label: function () {
|
||||
return this.name || "mi-devices mi.weather";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
if(node.sid) {
|
||||
$('#node-input-sid').val(node.sid);
|
||||
}
|
||||
|
||||
function changeGateway(model) {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
$('#node-input-sid').empty();
|
||||
for (sid in configNode.deviceList) {
|
||||
var device = configNode.deviceList[sid];
|
||||
if (device.internalModel === model) {
|
||||
$('#node-input-sid').append('<option value="' + sid + '">' + device.name + ' - ' + sid + '</option>');
|
||||
}
|
||||
}
|
||||
if(node.sid) {
|
||||
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-sid").change(function () {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val($('#node-input-sid option:selected').text());
|
||||
}
|
||||
});
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway("mi.weather");
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
node.sid = $("#node-input-sid").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-mi.weather">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
|
||||
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-mi.weather">
|
||||
<p>The Xiaomi Humidity & Temperature sensor node</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>
|
||||
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
|
||||
<hr>
|
||||
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">object</span></dt>
|
||||
<dd>Data from gateway when used as a filter (see below).</dd>
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device SID.</dd>
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the device is registred.</dd>
|
||||
</dl>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>Sample payload after incoming incoming message:</p>
|
||||
<p><pre>{
|
||||
cmd: "read_ack"
|
||||
model: "weather.v1"
|
||||
sid: "158d00010b7f1b"
|
||||
short_id: 8451
|
||||
data: {
|
||||
voltage:3005,
|
||||
temperature:23.25,
|
||||
humidity:56.99,
|
||||
pressure:981.26,
|
||||
batteryLevel: 34
|
||||
}
|
||||
}</pre>
|
||||
Where <code>humidy</code> is in percents, <code>pressure</code> in kPa, <code>batteryLevel</code> is a computed percentage of remaining battery.
|
||||
</p>
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-mi.switch', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"mi-devices-gateway configurator"},
|
||||
name: {value: ""},
|
||||
sid: {value: "", required: true}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
paletteLabel: "switch",
|
||||
icon: "mi-switch.png",
|
||||
label: function () {
|
||||
return this.name || "mi-devices mi.switch";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
if(node.sid) {
|
||||
$('#node-input-sid').val(node.sid);
|
||||
}
|
||||
|
||||
function changeGateway(model) {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
$('#node-input-sid').empty();
|
||||
for (sid in configNode.deviceList) {
|
||||
var device = configNode.deviceList[sid];
|
||||
if (device.internalModel === model) {
|
||||
$('#node-input-sid').append('<option value="' + sid + '">' + device.name + ' - ' + sid + '</option>');
|
||||
}
|
||||
}
|
||||
if(node.sid) {
|
||||
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-sid").change(function () {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val($('#node-input-sid option:selected').text());
|
||||
}
|
||||
});
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway("mi.switch");
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
node.sid = $("#node-input-sid").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-mi.switch">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
|
||||
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-mi.switch">
|
||||
<p>The Xiaomi Switch sensor node</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>
|
||||
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
|
||||
<hr>
|
||||
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">object</span></dt>
|
||||
<dd>Data from gateway when used as a filter (see below).</dd>
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device SID.</dd>
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the device is registred.</dd>
|
||||
</dl>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>Sample payload after incoming incoming message:</p>
|
||||
<p><pre>{
|
||||
cmd: "report"
|
||||
model: "switch"
|
||||
sid: "158d000128b124"
|
||||
short_id: 56773
|
||||
data: {
|
||||
status: "click",
|
||||
batteryLevel: 23
|
||||
}
|
||||
}</pre>
|
||||
Where <code>batteryLevel</code> is a computed percentage of remaining battery.
|
||||
</p>
|
||||
</script>
|
||||
11
dist/nodes/gateway-subdevices/index.js
vendored
@@ -1,11 +0,0 @@
|
||||
"use strict";
|
||||
const All_1 = require("./All");
|
||||
const Plug_1 = require("./Plug");
|
||||
const GatewaySubdevice_1 = require("./GatewaySubdevice");
|
||||
module.exports = (RED) => {
|
||||
All_1.default(RED);
|
||||
Plug_1.default(RED);
|
||||
["magnet", "motion", "sensor", "switch"].forEach((subdeviceType) => {
|
||||
GatewaySubdevice_1.default(RED, subdeviceType);
|
||||
});
|
||||
};
|
||||
46
dist/nodes/gateway/Gateway.js
vendored
@@ -1,46 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED) => {
|
||||
class Gateway {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.gatewayConf = RED.nodes.getNode(props.gateway);
|
||||
this.status({ fill: "red", shape: "ring", text: "offline" });
|
||||
if (this.gatewayConf.gateway) {
|
||||
this.gatewayOnline();
|
||||
}
|
||||
this.gatewayConf.on('gateway-online', () => this.gatewayOnline());
|
||||
this.gatewayConf.on('gateway-offline', () => this.gatewayOffline());
|
||||
this.setMessageListener();
|
||||
}
|
||||
gatewayOnline() {
|
||||
this.status({ fill: "blue", shape: "dot", text: "online" });
|
||||
}
|
||||
gatewayOffline() {
|
||||
this.status({ fill: "red", shape: "ring", text: "offline" });
|
||||
}
|
||||
setMessageListener() {
|
||||
this.on('input', (msg) => {
|
||||
if (this.gatewayConf.gateway) {
|
||||
var payload = msg.payload;
|
||||
// Input from gateway
|
||||
if (payload.sid && payload.sid == this.gatewayConf.gateway.sid) {
|
||||
if (payload.data.rgb) {
|
||||
/*var decomposed = miDevicesUtils.computeColor(payload.data.rgb);
|
||||
payload.data.brightness = decomposed.brightness;
|
||||
payload.data.color = decomposed.color;*/
|
||||
}
|
||||
this.send(msg);
|
||||
}
|
||||
else {
|
||||
msg.sid = this.gatewayConf.gateway.sid;
|
||||
msg.gateway = this.gatewayConf.gateway;
|
||||
this.send(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway`, Gateway);
|
||||
};
|
||||
58
dist/nodes/gateway/GatewayConfigurator.js
vendored
@@ -1,58 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
const GatewayServer_1 = require("../../devices/gateway/GatewayServer");
|
||||
exports.default = (RED) => {
|
||||
class GatewayConfigurator {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
let { sid, key, deviceList } = props;
|
||||
this.sid = sid;
|
||||
this.key = key;
|
||||
this.deviceList = deviceList;
|
||||
let server = GatewayServer_1.GatewayServer.getInstance();
|
||||
if (this.sid) {
|
||||
this.setGateway();
|
||||
}
|
||||
server.on('gateway-online', (sid) => {
|
||||
if (sid === this.sid) {
|
||||
this.setGateway();
|
||||
this.emit('gateway-online');
|
||||
}
|
||||
});
|
||||
server.on('gateway-offline', (sid) => {
|
||||
if (sid === this.sid) {
|
||||
this._gateway = null;
|
||||
this.emit('gateway-offline');
|
||||
}
|
||||
});
|
||||
}
|
||||
setGateway() {
|
||||
this._gateway = GatewayServer_1.GatewayServer.getInstance().getGateway(this.sid);
|
||||
if (this._gateway) {
|
||||
this._gateway.password = this.key;
|
||||
this._gateway.on("subdevice-values-updated", (sidOrMessage) => {
|
||||
let sid = sidOrMessage.sid || sidOrMessage;
|
||||
let subdevice = this._gateway.getSubdevice(sid);
|
||||
if (subdevice) {
|
||||
(sidOrMessage.data ? Object.keys(sidOrMessage.data) : []).forEach((key) => {
|
||||
subdevice[key] = sidOrMessage.data[key];
|
||||
});
|
||||
this.emit('subdevice-update', subdevice);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
get gateway() {
|
||||
return this._gateway;
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway configurator`, GatewayConfigurator, {
|
||||
settings: {
|
||||
miDevicesGatewayConfiguratorDiscoveredGateways: {
|
||||
value: GatewayServer_1.GatewayServer.getInstance().gateways,
|
||||
exportable: true
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
27
dist/nodes/gateway/GatewayIn.js
vendored
@@ -1,27 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED) => {
|
||||
class GatewayIn {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.gatewayConf = RED.nodes.getNode(props.gateway);
|
||||
this.status({ fill: "red", shape: "ring", text: "offline" });
|
||||
if (this.gatewayConf.gateway) {
|
||||
this.gatewayOnline();
|
||||
}
|
||||
this.gatewayConf.on('gateway-online', () => this.gatewayOnline());
|
||||
this.gatewayConf.on('gateway-offline', () => this.gatewayOffline());
|
||||
}
|
||||
gatewayOnline() {
|
||||
this.status({ fill: "blue", shape: "dot", text: "online" });
|
||||
this.gatewayConf.on('subdevice-update', (subdevice) => {
|
||||
this.send({ payload: subdevice });
|
||||
});
|
||||
}
|
||||
gatewayOffline() {
|
||||
this.status({ fill: "red", shape: "ring", text: "offline" });
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway in`, GatewayIn);
|
||||
};
|
||||
35
dist/nodes/gateway/GatewayOut.js
vendored
@@ -1,35 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
const GatewayServer_1 = require("../../devices/gateway/GatewayServer");
|
||||
exports.default = (RED) => {
|
||||
class GatewayOut {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.setMessageListener();
|
||||
}
|
||||
setMessageListener() {
|
||||
this.on("input", (msg) => {
|
||||
if (msg.hasOwnProperty("payload") && msg.hasOwnProperty("gateway")) {
|
||||
let gateway = GatewayServer_1.GatewayServer.getInstance().getGateway(msg.gateway.sid);
|
||||
if (gateway) {
|
||||
if (msg.payload.cmd) {
|
||||
gateway.send(msg);
|
||||
}
|
||||
else if (msg.payload.action) {
|
||||
switch (msg.payload.action) {
|
||||
case 'setLight':
|
||||
gateway.setLight(msg.payload.brightness, msg.payload.color);
|
||||
break;
|
||||
case 'playSound':
|
||||
gateway.playSound(msg.payload.mid, msg.payload.volume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway out`, GatewayOut);
|
||||
};
|
||||
295
dist/nodes/gateway/index.html
vendored
@@ -1,295 +0,0 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-gateway configurator', {
|
||||
category: 'config',
|
||||
defaults: {
|
||||
name: {value: ""},
|
||||
sid: {value: ""},
|
||||
key: { value: "" },
|
||||
deviceList: {value:{}}
|
||||
},
|
||||
paletteLabel: "gateway configurator",
|
||||
label: function () {
|
||||
return this.name || "gateway configurator";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var foundGateways = RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways;
|
||||
Object.keys(foundGateways).forEach(function(sid) {
|
||||
var gateway = foundGateways[sid];
|
||||
$('#discovered-gateways').append('<option value="' + gateway.sid + '">' + gateway.sid + ' - ' + gateway.ip + '</option>');
|
||||
});
|
||||
var node = this;
|
||||
|
||||
var devicesConfig = {
|
||||
"mi.weather": {label:"weather", icon:"icons/node-red-contrib-mi-devices/thermometer-icon.png"},
|
||||
"mi.magnet": {label:"magnet", icon:"icons/node-red-contrib-mi-devices/door-icon.png"},
|
||||
"mi.motion": {label:"motion", icon:"icons/node-red-contrib-mi-devices/motion-icon.png"},
|
||||
"mi.switch": {label:"switch", icon:"icons/node-red-contrib-mi-devices/mi-switch.png"},
|
||||
"mi.plug": {label:"plug zigbee", icon:"icons/node-red-contrib-mi-devices/outlet-icon.png"}
|
||||
};
|
||||
|
||||
$("#node-config-input-subdevices").css('min-height','250px').css('min-width','450px').editableList({
|
||||
addItem: function(container, i, device) {
|
||||
var row = container;
|
||||
$('<label/>',{for:"node-config-input-sid-"+i, style:"margin-left: 3px; width: 15px;vertical-align:middle"}).appendTo(row);
|
||||
var sid = $('<input/>',{id:"node-config-input-sid-"+i,type:"text", placeholder:"SID", style:"width:auto;vertical-align:top"}).appendTo(row);
|
||||
sid.typedInput({
|
||||
default: 'mi.weather',
|
||||
types: Object.keys(devicesConfig).map(function(type) {
|
||||
var cleanType = devicesConfig[type];
|
||||
cleanType.value = type;
|
||||
return cleanType;
|
||||
})
|
||||
});
|
||||
$('<label/>',{for:"node-config-input-desc-"+i, style:"margin-left: 7px; width: 20px;vertical-align:middle"}).html('<i class="fa fa-pencil-square-o"></i>').appendTo(row);
|
||||
var desc = $('<input/>',{id:"node-config-input-desc-"+i, type:"text", placeholder:"name", style:"width:auto;vertical-align:top"}).appendTo(row);
|
||||
sid.typedInput('value', device.sid);
|
||||
sid.typedInput('type', device.internalModel);
|
||||
desc.val(device.name);
|
||||
container.parent().attr("data-sid", device.sid);
|
||||
},
|
||||
removeItem: function(device) {
|
||||
$("#node-config-input-subdevices").find("[data-sid=" + device.sid + "]").remove();
|
||||
},
|
||||
sortable: false,
|
||||
removable: true
|
||||
});
|
||||
|
||||
$('#discovered-gateways').on('change', function() {
|
||||
var sid = $('#discovered-gateways').val();
|
||||
|
||||
$('#input-subdevices > *').remove();
|
||||
var gateway = sid && RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways[sid];
|
||||
$("#node-config-input-sid").val(gateway && gateway.sid);
|
||||
$("#node-config-input-key").val(gateway && gateway.key);
|
||||
|
||||
$("#node-config-input-subdevices").editableList('items').each(function(i, elt) {
|
||||
$("#node-config-input-subdevices").editableList('removeItem', {sid: $(elt).find("#node-config-input-sid-"+i).val()});
|
||||
});
|
||||
var subdevices = gateway && Object.keys(gateway.subdevices).map(function(sid) { return gateway.subdevices[sid]; });
|
||||
subdevices && subdevices.sort(function(a, b) { return a.internalModel > b.internalModel; }).forEach(function(device) {
|
||||
if(!devicesConfig[device.internalModel] || !device.sid) {
|
||||
return;
|
||||
}
|
||||
if(node.deviceList[device.sid]) {
|
||||
device.name = node.deviceList[device.sid].name;
|
||||
}
|
||||
$("#node-config-input-subdevices").editableList('addItem', {
|
||||
sid: device.sid,
|
||||
internalModel: device.internalModel,
|
||||
name: device.name
|
||||
});
|
||||
});
|
||||
var listHeight = $("#node-config-input-subdevices").editableList('items').size() * 51 + 50;
|
||||
$("#node-config-input-subdevices").editableList('height', listHeight);
|
||||
});
|
||||
|
||||
Object.keys(this.deviceList || {}).forEach(function(sid) {
|
||||
var device = node.deviceList[sid];
|
||||
$("#node-config-input-subdevices").editableList('addItem', {
|
||||
sid: sid,
|
||||
internalModel: device.internalModel,
|
||||
name: device.name
|
||||
});
|
||||
});
|
||||
|
||||
var listHeight = $("#node-config-input-subdevices").editableList('items').size() * 51 + 50;
|
||||
$("#node-config-input-subdevices").editableList('height', listHeight);
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
var devices = $("#node-config-input-subdevices").editableList('items');
|
||||
|
||||
devices.each(function(i, elt) {
|
||||
var deviceElement = $(elt);
|
||||
var sid = deviceElement.find("#node-config-input-sid-"+i).val();
|
||||
var desc = deviceElement.find("#node-config-input-desc-"+i).val();
|
||||
var internalModel = deviceElement.find("#node-config-input-sid-"+i).typedInput('type');
|
||||
node.deviceList[sid] = {internalModel: internalModel, name: desc};
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-gateway configurator">
|
||||
<div class="form-row">
|
||||
<label for="discovered-gateways"><i class="fa fa-search"></i> Found gateways</label>
|
||||
<select id="discovered-gateways">
|
||||
<option>- Select -</option>
|
||||
</select>
|
||||
</div>
|
||||
<hr>
|
||||
<h4>Gateway</h4>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-config-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-sid"><i class="fa fa-barcode"></i> SID</label>
|
||||
<input type="text" id="node-config-input-sid" placeholder="sid">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-gatewayKey"><i class="fa fa-key"></i> Key</label>
|
||||
<input type="text" id="node-config-input-key" placeholder="Key">
|
||||
</div>
|
||||
<h4>Devices</h4>
|
||||
<div class="form-row node-config-input-subdevices">
|
||||
<ol id="node-config-input-subdevices"></ol>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-gateway configurator">
|
||||
<p>Gateway configuration for Xiaomi nodes.</p>
|
||||
<h3>Details</h3>
|
||||
<p>This configuration node is used by the Xiaomi device nodes. Here you can add
|
||||
devices with their device-id (SID), type and a description.</p>
|
||||
<p>At the moment the following devices are supported:
|
||||
<lu>
|
||||
<li>Humidity & Temperature sensor [sensor ht/]</li>
|
||||
<li>Body motion sensor [motion]</li>
|
||||
<li>Magnet contact sensor [contact]</li>
|
||||
<li>Wall socket plug (zigbee) [plug]</li>
|
||||
<li>Push button [switch]</li>
|
||||
</lu>
|
||||
</p>
|
||||
<p>To be able to receive messages from the Xiaomi gateway, you need to set the gateway
|
||||
in developer mode. Once in developer mode, the gateway sends JSON messages over the network as
|
||||
UDP packages. On the internet their are a lot of guides on how to put the gateway in developer mode.</p>
|
||||
<p>If you want to use the wall sockets, you need to set the key from the gateway. The key can be
|
||||
retrieved via the Xiaomi Home App when in developer mode. Enter the key here and it is used
|
||||
together with the token from the gateway's heartbeat message to recalculate the key to switch
|
||||
the plug. If you do not specify a key, the plug-node can not be used.</p>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-gateway', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"mi-devices-gateway configurator"},
|
||||
name: {value: ""}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
outputLabels: ["Gateway"],
|
||||
paletteLabel: "gateway",
|
||||
icon: "mijia.png",
|
||||
label: function () {
|
||||
return this.name || "gateway";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-gateway">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="fa fa-wrench"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-gateway">
|
||||
<p>The gateway itself.</p>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<li>Devices output
|
||||
<dl class="message-properties">
|
||||
<dt>gateway <span class="property-type">mi-devices-gateway configurator</span></dt>
|
||||
<dd>The gateway.</dd>
|
||||
</dl>
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">json</span></dt>
|
||||
<dd>Data from gateway, with computed data.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ol>
|
||||
</script>
|
||||
<script type="text/x-red" data-template-name="mi-devices-gateway in">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="fa fa-wrench"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-gateway in">
|
||||
<p>A Xiaomi Gateway input node, that produces a <code>msg.payload</code> containing a
|
||||
string with the gateway message content.
|
||||
</p>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-gateway in',{
|
||||
category: 'xiaomi in out',
|
||||
color: '#087F8A',
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
gateway: {value:"", type:"mi-devices-gateway configurator"}
|
||||
},
|
||||
inputs:0,
|
||||
outputs:1,
|
||||
paletteLabel: "gateway in",
|
||||
icon: "mijia-io.png",
|
||||
label: function() {
|
||||
return this.name||"gateway in";
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name?"node_label_italic":"";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
function changeGateway() {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val(configNode.name);
|
||||
}
|
||||
$('#node-input-ip').val(configNode.ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="text/x-red" data-template-name="mi-devices-gateway out">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-gateway out">
|
||||
<p>This node sends <code>msg.payload</code> to <code>msg.gateway</code> Xiaomi Gateway.</p>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-gateway out',{
|
||||
category: 'xiaomi in out',
|
||||
color: '#087F8A',
|
||||
defaults: {
|
||||
name: {value:""}
|
||||
},
|
||||
inputs:1,
|
||||
outputs:0,
|
||||
paletteLabel: "gateway out",
|
||||
icon: "mijia-io.png",
|
||||
align: "right",
|
||||
label: function() {
|
||||
return this.name||"gateway out";
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name?"node_label_italic":"";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
13
dist/nodes/gateway/index.js
vendored
@@ -1,13 +0,0 @@
|
||||
"use strict";
|
||||
const GatewayServer_1 = require("../../devices/gateway/GatewayServer");
|
||||
const GatewayConfigurator_1 = require("./GatewayConfigurator");
|
||||
const Gateway_1 = require("./Gateway");
|
||||
const GatewayIn_1 = require("./GatewayIn");
|
||||
const GatewayOut_1 = require("./GatewayOut");
|
||||
module.exports = (RED) => {
|
||||
GatewayServer_1.GatewayServer.getInstance().discover();
|
||||
GatewayConfigurator_1.default(RED);
|
||||
Gateway_1.default(RED);
|
||||
GatewayIn_1.default(RED);
|
||||
GatewayOut_1.default(RED);
|
||||
};
|
||||
139
dist/nodes/plug-wifi/index.js
vendored
@@ -1,139 +0,0 @@
|
||||
"use strict";
|
||||
const constants_1 = require("../constants");
|
||||
const miio = require("miio");
|
||||
module.exports = (RED) => {
|
||||
var connectionState = "timeout";
|
||||
var retryTimer;
|
||||
var delayedStatusMsgTimer;
|
||||
function XiaomiPlugWifiNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
this.ip = config.ip;
|
||||
this.plug = null;
|
||||
this.status({ fill: "yellow", shape: "dot", text: "connecting" });
|
||||
miio.device({ address: this.ip })
|
||||
.then((plug) => {
|
||||
this.plug = plug;
|
||||
this.status({ fill: "green", shape: "dot", text: "connected" });
|
||||
connectionState = "connected";
|
||||
delayedStatusMsgUpdate();
|
||||
this.plug.on('propertyChanged', (e) => {
|
||||
if (e.property === "power") {
|
||||
if (e.value['0']) {
|
||||
setState("on");
|
||||
}
|
||||
else {
|
||||
setState("off");
|
||||
}
|
||||
}
|
||||
});
|
||||
watchdog();
|
||||
})
|
||||
.catch((error) => {
|
||||
connectionState = "reconnecting";
|
||||
watchdog();
|
||||
});
|
||||
this.on('input', (msg) => {
|
||||
var payload = msg.payload;
|
||||
if (connectionState === "connected") {
|
||||
if (payload == 'on') {
|
||||
this.plug.setPower(true);
|
||||
}
|
||||
if (payload == 'off') {
|
||||
this.plug.setPower(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.on('close', (done) => {
|
||||
if (retryTimer) {
|
||||
clearTimeout(retryTimer);
|
||||
}
|
||||
if (delayedStatusMsgTimer) {
|
||||
clearTimeout(delayedStatusMsgTimer);
|
||||
}
|
||||
if (this.plug) {
|
||||
this.plug.destroy();
|
||||
}
|
||||
done();
|
||||
});
|
||||
var setState = (state) => {
|
||||
if (this.plug) {
|
||||
let status = {
|
||||
payload: {
|
||||
id: this.plug.id,
|
||||
type: this.plug.type,
|
||||
model: this.plug.model,
|
||||
capabilities: this.plug.capabilities,
|
||||
address: this.plug.address,
|
||||
port: this.plug.port,
|
||||
power: this.plug.power(),
|
||||
state: state
|
||||
}
|
||||
};
|
||||
this.send(status);
|
||||
}
|
||||
};
|
||||
var delayedStatusMsgUpdate = () => {
|
||||
delayedStatusMsgTimer = setTimeout(() => {
|
||||
if (this.plug.power()['0']) {
|
||||
setState("on");
|
||||
}
|
||||
else {
|
||||
setState("off");
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
var discoverDevice = () => {
|
||||
miio.device({ address: this.ip })
|
||||
.then((plug) => {
|
||||
if (this.plug == null) {
|
||||
this.plug = plug;
|
||||
this.plug.on('propertyChanged', (e) => {
|
||||
if (e.property === "power") {
|
||||
if (e.value['0']) {
|
||||
setState("on");
|
||||
}
|
||||
else {
|
||||
setState("off");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (connectionState === "reconnecting") {
|
||||
this.status({ fill: "green", shape: "dot", text: "connected" });
|
||||
connectionState = "connected";
|
||||
delayedStatusMsgUpdate();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
connectionState = "reconnecting";
|
||||
if (this.plug) {
|
||||
this.plug.destroy();
|
||||
this.plug = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
var watchdog = () => {
|
||||
var node = this;
|
||||
function retryTimer() {
|
||||
discoverDevice();
|
||||
if (connectionState === "reconnecting") {
|
||||
node.status({ fill: "red", shape: "dot", text: "reconnecting" });
|
||||
}
|
||||
setTimeout(retryTimer, 30000);
|
||||
}
|
||||
setTimeout(retryTimer, 30000);
|
||||
};
|
||||
}
|
||||
process.on('unhandledRejection', function (reason, p) {
|
||||
// console.log("Possibly Unhandled Rejection at: Promise ", p, " reason: ", reason);
|
||||
var message = reason + "";
|
||||
if (message.indexOf("Call to device timed out") >= 0) {
|
||||
if (this.plug) {
|
||||
console.log("Issue with miio package; discard plug and reconnect.");
|
||||
this.plug.destroy();
|
||||
this.plug = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-wifi-plug`, XiaomiPlugWifiNode);
|
||||
};
|
||||
43
dist/nodes/yeelight/YeelightConfigurator.js
vendored
@@ -1,43 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
const YeelightServer_1 = require("../../devices/yeelight/YeelightServer");
|
||||
exports.default = (RED) => {
|
||||
class YeelightConfigurator {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
let { sid } = props;
|
||||
this.sid = parseInt(sid);
|
||||
if (this.sid) {
|
||||
this.setBulb();
|
||||
}
|
||||
let server = YeelightServer_1.YeelightServer.getInstance();
|
||||
server.on('yeelight-online', (sid) => {
|
||||
if (sid === this.sid) {
|
||||
this.setBulb();
|
||||
this.emit('bulb-online');
|
||||
}
|
||||
});
|
||||
server.on('yeelight-offline', (sid) => {
|
||||
if (sid === this.sid) {
|
||||
this._bulb = null;
|
||||
this.emit('bulb-offline');
|
||||
}
|
||||
});
|
||||
}
|
||||
setBulb() {
|
||||
this._bulb = YeelightServer_1.YeelightServer.getInstance().getBulb(this.sid);
|
||||
}
|
||||
get bulb() {
|
||||
return this._bulb;
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-yeelight configurator`, YeelightConfigurator, {
|
||||
settings: {
|
||||
miDevicesYeelightConfiguratorDiscoveredBulbs: {
|
||||
value: YeelightServer_1.YeelightServer.getInstance().bulbs,
|
||||
exportable: true
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
64
dist/nodes/yeelight/YeelightOut.js
vendored
@@ -1,64 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const constants_1 = require("../constants");
|
||||
exports.default = (RED) => {
|
||||
class YeelightOut {
|
||||
constructor(props) {
|
||||
RED.nodes.createNode(this, props);
|
||||
this.yeelightConf = RED.nodes.getNode(props.yeelight);
|
||||
this.status({ fill: "red", shape: "ring", text: "offline" });
|
||||
if (this.yeelightConf.bulb) {
|
||||
this.yeelightOnline();
|
||||
}
|
||||
this.yeelightConf.on('bulb-online', () => this.yeelightOnline());
|
||||
this.yeelightConf.on('bulb-offline', () => this.yeelightOffline());
|
||||
this.setListener();
|
||||
}
|
||||
yeelightOnline() {
|
||||
this.status({ fill: "blue", shape: "dot", text: "online" });
|
||||
}
|
||||
yeelightOffline() {
|
||||
this.status({ fill: "red", shape: "ring", text: "offline" });
|
||||
}
|
||||
setListener() {
|
||||
this.on("input", (msg) => {
|
||||
let bulb = this.yeelightConf.bulb;
|
||||
if (msg.hasOwnProperty("payload") && bulb) {
|
||||
switch (msg.payload.action) {
|
||||
case 'turn_on':
|
||||
bulb.turnOn();
|
||||
break;
|
||||
case 'turn_off':
|
||||
bulb.turnOff();
|
||||
break;
|
||||
case 'toggle':
|
||||
bulb.toggle();
|
||||
break;
|
||||
case 'setLight':
|
||||
if (msg.payload.color !== undefined) {
|
||||
let rgb = msg.payload.color.blue | (msg.payload.color.green << 8) | (msg.payload.color.red << 16);
|
||||
let hex = '#' + (0x1000000 + rgb).toString(16).slice(1);
|
||||
bulb.setRGB(hex);
|
||||
}
|
||||
(msg.payload.brightness !== undefined) && bulb.setBrightness(Math.max(1, msg.payload.brightness));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
/*(<any> this).on('input', (msg) => {
|
||||
if (this.yeelightConf.bulb) {
|
||||
|
||||
|
||||
if(msg.payload.color !== undefined) {
|
||||
// TODO: revoir la couleur
|
||||
this.yeelightConf.bulb.setRGB(msg.payload.color);
|
||||
}
|
||||
if(msg.payload.brightness !== undefined) {
|
||||
this.yeelightConf.bulb.setBrightness(msg.payload.brightness);
|
||||
}
|
||||
}
|
||||
});*/
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-yeelight out`, YeelightOut);
|
||||
};
|
||||
BIN
dist/nodes/yeelight/icons/mi-yeelight.png
vendored
|
Before Width: | Height: | Size: 1.5 KiB |
160
dist/nodes/yeelight/index.html
vendored
@@ -1,160 +0,0 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-yeelight configurator', {
|
||||
category: 'config',
|
||||
defaults: {
|
||||
name: {value: ""},
|
||||
sid: {value: ""}
|
||||
},
|
||||
label: function () {
|
||||
return this.name || "yeelight conf";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var foundBulbs = RED.settings.miDevicesYeelightConfiguratorDiscoveredBulbs;
|
||||
Object.keys(foundBulbs).forEach(function(sid) {
|
||||
var bulb = foundBulbs[sid];
|
||||
$('#discovered-bulbs').append('<option value="' + bulb.sid + '">' + (bulb.name || bulb.sid) + ' - ' + bulb.model + ' - ' + bulb.ip + '</option>');
|
||||
});
|
||||
$('#discovered-bulbs').on('change', function() {
|
||||
var sid = $('#discovered-bulbs').val();
|
||||
var bulb = foundBulbs[sid];
|
||||
$("#node-config-input-name").val(bulb && bulb.name);
|
||||
$("#node-config-input-sid").val(bulb && bulb.sid);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-yeelight configurator">
|
||||
<div class="form-row">
|
||||
<label for="discovered-bulbs"><i class="fa fa-search"></i> Found bulbs</label>
|
||||
<select id="discovered-bulbs">
|
||||
<option>- Select -</option>
|
||||
</select>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-config-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-sid"><i class="fa fa-barcode"></i> SID</label>
|
||||
<input type="text" id="node-config-input-sid" placeholder="sid">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-yeelight configurator">
|
||||
<p>Xiaomi Yeelight configuration node.</p>
|
||||
<h3>Details</h3>
|
||||
<p>This configuration node is used by the Yeelight nodes. Here you can add
|
||||
devices with their device-id (SID), type and a description.</p>
|
||||
<p>At the moment the following devices are supported:
|
||||
<lu>
|
||||
<li>Humidity & Temperature sensor [sensor ht/]</li>
|
||||
<li>Body motion sensor [motion]</li>
|
||||
<li>Magnet contact sensor [contact]</li>
|
||||
<li>Wall socket plug (zigbee) [plug]</li>
|
||||
<li>Push button [switch]</li>
|
||||
</lu>
|
||||
</p>
|
||||
<p>To be able to receive messages from the Xiaomi gateway, you need to set the gateway
|
||||
in developer mode. Once in developer mode, the gateway sends JSON messages over the network as
|
||||
UDP packages. On the internet their are a lot of guides on how to put the gateway in developer mode.</p>
|
||||
<p>If you want to use the wall sockets, you need to set the key from the gateway. The key can be
|
||||
retrieved via the Xiaomi Home App when in developer mode. Enter the key here and it is used
|
||||
together with the token from the gateway's heartbeat message to recalculate the key to switch
|
||||
the plug. If you do not specify a key, the plug-node can not be used.</p>
|
||||
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-yeelight out', {
|
||||
category: 'xiaomi in out',
|
||||
color: '#087F8A',
|
||||
defaults: {
|
||||
name: {value: ""},
|
||||
yeelight: {value:"", type:"mi-devices-yeelight configurator"}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 0,
|
||||
paletteLabel: "yeelight out",
|
||||
icon: "mi-yeelight.png",
|
||||
align: "right",
|
||||
label: function () {
|
||||
return this.name || "yeelight out";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
function changeGateway() {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val(configNode.name);
|
||||
}
|
||||
$('#node-input-ip').val(configNode.ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-yeelight out">
|
||||
<div class="form-row">
|
||||
<label for="node-input-yeelight"><i class="fa fa-wrench"></i> Yeelight</label>
|
||||
<input type="text" id="node-input-yeelight" placeholder="yeelight">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-yeelight out">
|
||||
<p>The Xiaomi Yeelight node</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>
|
||||
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
|
||||
<hr>
|
||||
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
|
||||
<hr>
|
||||
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">object</span></dt>
|
||||
<dd>Data from gateway when used as a filter (see below).</dd>
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device SID.</dd>
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
|
||||
</dl>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>Sample message:</p>
|
||||
<p><pre>{
|
||||
cmd: "report"
|
||||
model: "switch"
|
||||
sid: "158d000128b124"
|
||||
short_id: 56773
|
||||
data: {
|
||||
status: "click",
|
||||
batteryLevel: 23
|
||||
}
|
||||
}</pre></p>
|
||||
|
||||
</script>
|
||||
9
dist/nodes/yeelight/index.js
vendored
@@ -1,9 +0,0 @@
|
||||
"use strict";
|
||||
const YeelightServer_1 = require("../../devices/yeelight/YeelightServer");
|
||||
const YeelightConfigurator_1 = require("./YeelightConfigurator");
|
||||
const YeelightOut_1 = require("./YeelightOut");
|
||||
module.exports = (RED) => {
|
||||
YeelightServer_1.YeelightServer.getInstance().discover();
|
||||
YeelightConfigurator_1.default(RED);
|
||||
YeelightOut_1.default(RED);
|
||||
};
|
||||
23
dist/utils/Color.js
vendored
@@ -1,23 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class Color {
|
||||
static toValue(red, green, blue, brightness) {
|
||||
return (brightness !== undefined ? 256 * 256 * 256 * brightness : 0) + (256 * 256 * red) + (256 * green) + blue;
|
||||
}
|
||||
static fromValue(rgb) {
|
||||
var blue = rgb % 256;
|
||||
rgb = Math.max(rgb - blue, 0);
|
||||
var green = rgb % (256 * 256);
|
||||
rgb = Math.max(rgb - green, 0);
|
||||
green /= 256;
|
||||
var red = rgb % (256 * 256 * 256);
|
||||
rgb = Math.max(rgb - red, 0);
|
||||
red /= 256 * 256;
|
||||
var brightness = rgb / (256 * 256 * 256);
|
||||
return {
|
||||
brightness: brightness,
|
||||
color: { red: red, green: green, blue: blue }
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.Color = Color;
|
||||
6
dist/utils/index.js
vendored
@@ -1,6 +0,0 @@
|
||||
"use strict";
|
||||
function __export(m) {
|
||||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__export(require("./Color"));
|
||||
|
Before Width: | Height: | Size: 1016 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 990 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 842 B |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1016 B After Width: | Height: | Size: 1016 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 990 B After Width: | Height: | Size: 990 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,29 +1,30 @@
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions read">
|
||||
<!-- The Read Node -->
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions read">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions read">
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions read">
|
||||
<p>Ask the gateway to read the report of the input device.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dt>sid
|
||||
<span class="property-type">string</span>
|
||||
</dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<p class="node-ports">Message to connect to a gateway out node.</p>
|
||||
<p class="node-ports">
|
||||
Message to connect to a gateway out node.
|
||||
</p>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions read',{
|
||||
RED.nodes.registerType('xiaomi-actions read',{
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -34,37 +35,31 @@
|
||||
paletteLabel: "read",
|
||||
icon: "mi-read.png",
|
||||
label: function() {
|
||||
return this.name||"mi-devices read";
|
||||
return this.name||"read";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions get_id_list">
|
||||
<!-- The get ids Node -->
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions get_id_list">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions get_id_list">
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions get_id_list">
|
||||
<p>Ask the gateway to the list of devices ids.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<p class="node-ports">Message to connect to a gateway out node.</p>
|
||||
<ol class="node-ports">
|
||||
<li>Message to connect to a gateway out node.</li>
|
||||
</ol>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions get_id_list',{
|
||||
RED.nodes.registerType('xiaomi-actions get_id_list',{
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -75,37 +70,44 @@
|
||||
paletteLabel: "get id list",
|
||||
icon: "mi-list.png",
|
||||
label: function() {
|
||||
return this.name||"mi-devices get id list";
|
||||
return this.name||"get id list";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions click">
|
||||
|
||||
<!-- The Single click Node -->
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions click">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions click">
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions click">
|
||||
<p>Virtual single click for switch.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dt>sid
|
||||
<span class="property-type">string</span>
|
||||
</dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
<dt>gateway
|
||||
<span class="property-type">xiaomi-configurator</span>
|
||||
</dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<p class="node-ports">Message to connect to a gateway out node.</p>
|
||||
<ol class="node-ports">
|
||||
<li>Message to connect to a gateway out node.</li>
|
||||
</ol>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions click',{
|
||||
RED.nodes.registerType('xiaomi-actions click',{
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -116,37 +118,43 @@
|
||||
paletteLabel: "click",
|
||||
icon: "mi-click.png",
|
||||
label: function() {
|
||||
return this.name||"mi-devices click";
|
||||
return this.name||"click";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions double_click">
|
||||
<!-- The Double click Node -->
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions double_click">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions double_click">
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions double_click">
|
||||
<p>Virtual double click for switch.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dt>sid
|
||||
<span class="property-type">string</span>
|
||||
</dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
<dt>gateway
|
||||
<span class="property-type">xiaomi-configurator</span>
|
||||
</dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<p class="node-ports">Message to connect to a gateway out node.</p>
|
||||
<ol class="node-ports">
|
||||
<li>Message to connect to a gateway out node.</li>
|
||||
</ol>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions double_click',{
|
||||
RED.nodes.registerType('xiaomi-actions double_click',{
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -155,16 +163,17 @@
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
paletteLabel: "double click",
|
||||
icon: "double-click.png",
|
||||
icon: "mi-double-click.png",
|
||||
label: function() {
|
||||
return this.name||"mi-devices double click";
|
||||
return this.name||"double click";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<!-- The Gateway light Node -->
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions light', {
|
||||
RED.nodes.registerType('xiaomi-actions gateway_light', {
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -194,7 +203,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions light">
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions gateway_light">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
@@ -209,7 +218,7 @@
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions light">
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions gateway_light">
|
||||
<p>Change the light of the gateway.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
@@ -241,9 +250,11 @@
|
||||
<li>Message to connect to a gateway out node.</li>
|
||||
</ol>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- The Gateway sound Node -->
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions gateway_play_sound', {
|
||||
RED.nodes.registerType('xiaomi-actions gateway_sound', {
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -261,7 +272,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions gateway_play_sound">
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions gateway_sound">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
@@ -298,10 +309,9 @@
|
||||
<label for="node-input-volume"><i class="icon-tag"></i> Volume</label>
|
||||
<input type="range" id="node-input-volume" min="0" max="100">
|
||||
</div>
|
||||
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions gateway_play_sound">
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions gateway_sound">
|
||||
<p>Play a sound on the gateway.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
@@ -320,34 +330,34 @@
|
||||
<ol class="node-ports">
|
||||
<li>Message to connect to a gateway out node.</li>
|
||||
</ol>
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<!-- The Gateway stop sound Node -->
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions gateway_stop_sound', {
|
||||
RED.nodes.registerType('xiaomi-actions gateway_stop_sound',{
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
name: {value: ""}
|
||||
name: {value:""}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
paletteLabel: "stop sound",
|
||||
icon: "mi-mute.png",
|
||||
label: function () {
|
||||
return this.name || "stop sound";
|
||||
label: function() {
|
||||
return this.name||"stop sound";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions gateway_stop_sound">
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions gateway_stop_sound">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions gateway_stop_sound">
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions gateway_stop_sound">
|
||||
<p>
|
||||
Stop current playing sound on the gateway.
|
||||
</p>
|
||||
@@ -356,35 +366,12 @@
|
||||
<ol class="node-ports">
|
||||
<li>Message to connect to a gateway out node.</li>
|
||||
</ol>
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions turn_on">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions turn_on">
|
||||
<p>Turn device on.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<p class="node-ports">Message to connect to a gateway out node.</p>
|
||||
</script>
|
||||
|
||||
<!-- The "on" Node -->
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions turn_on',{
|
||||
RED.nodes.registerType('xiaomi-actions on',{
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -392,40 +379,35 @@
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
paletteLabel: "turn on",
|
||||
paletteLabel: "on",
|
||||
icon: "mi-on.png",
|
||||
label: function() {
|
||||
return this.name||"mi-devices turn on";
|
||||
return this.name||"power on";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions turn_off">
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions on">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions turn_off">
|
||||
<p>Turn device off.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
|
||||
</dl>
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions on">
|
||||
<p>
|
||||
Turn input device to on.
|
||||
</p>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<p class="node-ports">Message to connect to a gateway out node.</p>
|
||||
<ol class="node-ports">
|
||||
<li>Message to connect to a gateway/yeelight out node.</li>
|
||||
</ol>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- The "off" Node -->
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions turn_off',{
|
||||
RED.nodes.registerType('xiaomi-actions off',{
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -433,40 +415,35 @@
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
paletteLabel: "turn off",
|
||||
paletteLabel: "off",
|
||||
icon: "mi-off.png",
|
||||
label: function() {
|
||||
return this.name||"mi-devices turn off";
|
||||
return this.name||"power off";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/x-red" data-template-name="mi-devices-actions toggle">
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions off">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="mi-devices-actions toggle">
|
||||
<p>Toggle device.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device <code>sid</code> to ask the report.</dd>
|
||||
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>mi-devices-gateway configurator</code> object where the input device is registred.</dd>
|
||||
</dl>
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions off">
|
||||
<p>
|
||||
Turn input device to off.
|
||||
</p>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<p class="node-ports">Message to connect to a gateway out node.</p>
|
||||
<ol class="node-ports">
|
||||
<li>Message to connect to a gateway/yeelight out node.</li>
|
||||
</ol>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- The "toggle" Node -->
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('mi-devices-actions toggle',{
|
||||
RED.nodes.registerType('xiaomi-actions toggle',{
|
||||
category: 'xiaomi actions',
|
||||
color: '#64C4CD',
|
||||
defaults: {
|
||||
@@ -477,7 +454,22 @@
|
||||
paletteLabel: "toggle",
|
||||
icon: "mi-toggle.png",
|
||||
label: function() {
|
||||
return this.name||"mi-devices toggle";
|
||||
return this.name||"toggle power";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
<script type="text/x-red" data-template-name="xiaomi-actions toggle">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xiaomi-actions toggle">
|
||||
<p>Toggle device.</p>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<li>Message to connect to a gateway/yeelight out node.</li>
|
||||
</ol>
|
||||
</script>
|
||||
185
node-red-contrib-xiaomi-actions/xiaomi-actions.js
Normal file
@@ -0,0 +1,185 @@
|
||||
const miDevicesUtils = require('../src/utils');
|
||||
|
||||
module.exports = (RED) => {
|
||||
/*********************************************
|
||||
Read data from Gateway
|
||||
*********************************************/
|
||||
function XiaomiActionRead(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
|
||||
this.on('input', (msg) => {
|
||||
if(msg.sid) {
|
||||
msg.payload = { cmd: "read", sid: msg.sid };
|
||||
this.send(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions read", XiaomiActionRead);
|
||||
|
||||
/*********************************************
|
||||
Get registred ids of devices on gateway
|
||||
*********************************************/
|
||||
function XiaomiActionGetIdList(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
|
||||
this.on('input', (msg) => {
|
||||
msg.payload = { cmd: "get_id_list" };
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions get_id_list", XiaomiActionGetIdList);
|
||||
|
||||
/*********************************************
|
||||
Virtual single click on a button
|
||||
*********************************************/
|
||||
function XiaomiActionSingleClick(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
|
||||
this.on('input', (msg) => {
|
||||
msg.payload = {
|
||||
cmd: "write",
|
||||
data: { status: "click", sid: msg.sid }
|
||||
};
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions click", XiaomiActionSingleClick);
|
||||
|
||||
/*********************************************
|
||||
Virtual Double click on a button
|
||||
*********************************************/
|
||||
function XiaomiActionDoubleClick(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
|
||||
this.on('input', (msg) => {
|
||||
msg.payload = {
|
||||
cmd: "write",
|
||||
data: { status: "double_click", sid: msg.sid }
|
||||
};
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions double_click", XiaomiActionDoubleClick);
|
||||
|
||||
/*********************************************
|
||||
Set the gateway light
|
||||
*********************************************/
|
||||
function XiaomiActionGatewayLight(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
this.color = config.color;
|
||||
this.brightness = config.brightness;
|
||||
|
||||
this.on('input', (msg) => {
|
||||
let color = msg.color || this.color;
|
||||
let brightness = msg.brightness || this.brightness;
|
||||
if(msg.sid) {
|
||||
let rgb = miDevicesUtils.computeColorValue(color.red, color.green, color.blue, brightness);
|
||||
msg.payload = {
|
||||
cmd: "write",
|
||||
data: { rgb: rgb, sid: msg.sid }
|
||||
};
|
||||
}
|
||||
else {
|
||||
msg.payload = {
|
||||
color: miDevicesUtils.computeColorValue(color.red, color.green, color.blue),
|
||||
brightness: brightness
|
||||
};
|
||||
}
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions gateway_light", XiaomiActionGatewayLight);
|
||||
|
||||
/*********************************************
|
||||
Play a sound on the gateway
|
||||
*********************************************/
|
||||
function XiaomiActionGatewaySound(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
this.mid = config.mid;
|
||||
this.volume = config.volume;
|
||||
|
||||
this.on('input', (msg) => {
|
||||
msg.payload = {
|
||||
cmd: "write",
|
||||
data: {
|
||||
mid: parseInt(msg.mid || this.mid),
|
||||
volume: parseInt(msg.volume || this.volume),
|
||||
sid: msg.sid
|
||||
}
|
||||
};
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions gateway_sound", XiaomiActionGatewaySound);
|
||||
|
||||
/*********************************************
|
||||
Stop playing a sound on the gateway
|
||||
*********************************************/
|
||||
function XiaomiActionGatewayStopSound(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
|
||||
this.on('input', (msg) => {
|
||||
msg.payload = {
|
||||
cmd: "write",
|
||||
data: { mid: 1000, sid: msg.sid }
|
||||
};
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions gateway_stop_sound", XiaomiActionGatewayStopSound);
|
||||
|
||||
/*********************************************
|
||||
Turn device on
|
||||
*********************************************/
|
||||
function XiaomiActionPowerOn(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
|
||||
this.on('input', (msg) => {
|
||||
if(msg.sid){
|
||||
msg.payload = {
|
||||
cmd: "write",
|
||||
data: { status: "on", sid: msg.sid }
|
||||
};
|
||||
}
|
||||
else {
|
||||
msg.payload = "on";
|
||||
}
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions on", XiaomiActionPowerOn);
|
||||
|
||||
/*********************************************
|
||||
Turn device off
|
||||
*********************************************/
|
||||
function XiaomiActionPowerOff(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
|
||||
this.on('input', (msg) => {
|
||||
if(msg.sid){
|
||||
msg.payload = {
|
||||
cmd: "write",
|
||||
data: { status: "off", sid: msg.sid }
|
||||
};
|
||||
}
|
||||
else {
|
||||
msg.payload = "off";
|
||||
}
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions off", XiaomiActionPowerOff);
|
||||
|
||||
/*********************************************
|
||||
Toggle device
|
||||
*********************************************/
|
||||
function XiaomiActionToggle(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
|
||||
this.on('input', (msg) => {
|
||||
msg.payload = "toggle";
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("xiaomi-actions toggle", XiaomiActionToggle);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,9 +1,9 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('<%= NODES_PREFIX %>-all', {
|
||||
RED.nodes.registerType('xiaomi-all', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"<%= NODES_PREFIX %>-gateway configurator"},
|
||||
gateway: {value:"", type:"xiaomi-configurator"},
|
||||
name: {value: ""},
|
||||
onlyModels: {value: []},
|
||||
excludedSids: { value: []}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
function getOnlyModelsValue(input) {
|
||||
var cleanOnlyModels = [];
|
||||
input.forEach(function(value) {
|
||||
input.forEach((value) => {
|
||||
cleanOnlyModels = cleanOnlyModels.concat(value.split(','));
|
||||
});
|
||||
return cleanOnlyModels;
|
||||
@@ -35,11 +35,11 @@
|
||||
onlyModels = getOnlyModelsValue(onlyModels || $('#node-input-onlyModels').val() || []);
|
||||
excludedSids = excludedSids || $('#node-input-excludedSids').val() || [];
|
||||
$('#node-input-excludedSids').empty();
|
||||
for (sid in configNode.deviceList) {
|
||||
var device = configNode.deviceList[sid];
|
||||
if (onlyModels.length == 0 || onlyModels.indexOf(device.internalModel) >= 0) {
|
||||
var option = $('<option value="' + sid + '">' + device.name + '</option>');
|
||||
if(excludedSids && excludedSids.indexOf(sid) >= 0) {
|
||||
for (key in configNode.deviceList) {
|
||||
var device = configNode.deviceList[key];
|
||||
if (onlyModels.length == 0 || onlyModels.indexOf(device.model) >= 0) {
|
||||
var option = $('<option value="' + device.sid + '">' + device.desc + '</option>');
|
||||
if(excludedSids && excludedSids.indexOf(device.sid) >= 0) {
|
||||
option.prop('selected', true);
|
||||
}
|
||||
$('#node-input-excludedSids').append(option);
|
||||
@@ -65,7 +65,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-all">
|
||||
<script type="text/x-red" data-template-name="xiaomi-all">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
@@ -79,11 +79,13 @@
|
||||
<div class="form-row">
|
||||
<label for="node-input-onlyModels"><i class="icon-tag"></i> Only</label>
|
||||
<select multiple id="node-input-onlyModels">
|
||||
<option value="mi.weather">Temperature/humidty</option>
|
||||
<option value="mi.motion">Motion</option>
|
||||
<option value="mi.switch">Switches</option>
|
||||
<option value="mi.magnet">Magnets</option>
|
||||
<option value="mi.plug">Plugs</option>
|
||||
<option value="sensor_ht,weather.v1">Temperature/Humidty</option>
|
||||
<option value="natgas">Natgas/Alarm/Density</option>
|
||||
<option value="smoke">Smoke/Alarm/Density</option>
|
||||
<option value="motion">Motion</option>
|
||||
<option value="switch,sensor_switch.aq2">Switches</option>
|
||||
<option value="magnet,sensor_magnet.aq2">Contacts</option>
|
||||
<option value="plug">Plugs</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
@@ -92,7 +94,7 @@
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-all">
|
||||
<script type="text/x-red" data-help-name="xiaomi-all">
|
||||
<p>All devices registred in the gateway, except gateway itself.</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
50
node-red-contrib-xiaomi-all/xiaomi-all.js
Normal file
@@ -0,0 +1,50 @@
|
||||
module.exports = (RED) => {
|
||||
function getOnlyModelsValue(input) {
|
||||
var cleanOnlyModels = [];
|
||||
input.forEach((value) => {
|
||||
cleanOnlyModels = cleanOnlyModels.concat(value.split(','));
|
||||
});
|
||||
return cleanOnlyModels;
|
||||
}
|
||||
|
||||
function XiaomiAllNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
this.gateway = RED.nodes.getNode(config.gateway);
|
||||
this.onlyModels = getOnlyModelsValue(config.onlyModels || []);
|
||||
this.excludedSids = config.excludedSids;
|
||||
|
||||
|
||||
this.isDeviceValid = (device) => {
|
||||
if((!this.onlyModels || this.onlyModels.length == 0) && (!this.excludedSids || this.excludedSids.length == 0)) {
|
||||
return true;
|
||||
}
|
||||
// Is excluded
|
||||
if((this.excludedSids && this.excludedSids.length != 0) && this.excludedSids.indexOf(device.sid) >= 0) {
|
||||
return false;
|
||||
}
|
||||
if((this.onlyModels && this.onlyModels.length != 0) && this.onlyModels.indexOf(device.model) >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.gateway) {
|
||||
this.on('input', (msg) => {
|
||||
// Filter input
|
||||
if(msg.payload && msg.payload.model && msg.payload.sid) {
|
||||
if(!this.isDeviceValid(msg.payload)) {
|
||||
msg = null;
|
||||
}
|
||||
}
|
||||
// Prepare for request
|
||||
else {
|
||||
msg.payload = this.gateway.deviceList.filter((device) => this.isDeviceValid(device));
|
||||
}
|
||||
this.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
RED.nodes.registerType("xiaomi-all", XiaomiAllNode);
|
||||
}
|
||||
BIN
node-red-contrib-xiaomi-configurator/icons/door-tw-icon.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
node-red-contrib-xiaomi-configurator/icons/mi-tw-switch.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
node-red-contrib-xiaomi-configurator/icons/motion-tw-icon.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
node-red-contrib-xiaomi-configurator/icons/natgas-tw-icon.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
node-red-contrib-xiaomi-configurator/icons/outlet-tw-icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
node-red-contrib-xiaomi-configurator/icons/smoke-tw-icon.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
node-red-contrib-xiaomi-configurator/icons/switch-tw-icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
135
node-red-contrib-xiaomi-configurator/xiaomi-configurator.html
Normal file
@@ -0,0 +1,135 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('xiaomi-configurator', {
|
||||
category: 'config',
|
||||
defaults: {
|
||||
name: {value: ""},
|
||||
ip: {value: "", required: true},
|
||||
sid: {value: ""},
|
||||
deviceList: {value:[{ sid:"", desc:"", model:"plug"}]},
|
||||
key: {value: ""}
|
||||
},
|
||||
label: function () {
|
||||
return this.name || "xiaomi-configurator";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
var tw_sensor_ht = {value:"sensor_ht", label:"sensor ht", icon:"icons/node-red-contrib-mi-devices/thermometer-tw-icon.png"};
|
||||
var tw_natgas = {value:"natgas", label:"natgas", icon:"icons/node-red-contrib-mi-devices/natgas-tw-icon.png"};
|
||||
var tw_smoke = {value:"smoke", label:"smoke", icon:"icons/node-red-contrib-mi-devices/smoke-tw-icon.png"};
|
||||
var tw_magnet = {value:"magnet", label:"contact", icon:"icons/node-red-contrib-mi-devices/door-tw-icon.png"};
|
||||
var tw_motion = {value:"motion", label:"motion", icon:"icons/node-red-contrib-mi-devices/motion-tw-icon.png"};
|
||||
var tw_plug = {value:"plug", label:"plug", icon:"icons/node-red-contrib-mi-devices/outlet-tw-icon.png"};
|
||||
var tw_switch = {value:"switch", label:"switch", icon:"icons/node-red-contrib-mi-devices/mi-tw-switch.png"};
|
||||
|
||||
$("#node-config-input-devices").css('min-height','250px').css('min-width','450px').editableList({
|
||||
addItem: function(container, i, device) {
|
||||
if (!device.hasOwnProperty('model')) {
|
||||
device.model = 'sensor_ht';
|
||||
}
|
||||
var row = $('<div/>').appendTo(container);
|
||||
|
||||
$('<label/>',{for:"node-config-input-sid-"+i, style:"margin-left: 3px; width: 15px;vertical-align:middle"}).appendTo(row);
|
||||
var sid = $('<input/>',{id:"node-config-input-sid-"+i,type:"text", placeholder:"SID", style:"width:auto;vertical-align:top"}).appendTo(row);
|
||||
sid.typedInput({
|
||||
default: 'sensor_ht',
|
||||
types: [tw_sensor_ht, tw_natgas, tw_smoke, tw_magnet, tw_motion, tw_plug, tw_switch]
|
||||
});
|
||||
|
||||
$('<label/>',{for:"node-config-input-desc-"+i, style:"margin-left: 7px; width: 20px;vertical-align:middle"}).html('<i class="fa fa-pencil-square-o"></i>').appendTo(row);
|
||||
var desc = $('<input/>',{id:"node-config-input-desc-"+i, type:"text", placeholder:"description", style:"width:auto;vertical-align:top"}).appendTo(row);
|
||||
|
||||
sid.typedInput('value', device.sid);
|
||||
sid.typedInput('type', device.model);
|
||||
desc.val(device.desc);
|
||||
},
|
||||
resize: function() {
|
||||
},
|
||||
removeItem: function(opt) {
|
||||
},
|
||||
sortItems: function(rules) {
|
||||
},
|
||||
sortable: true,
|
||||
removable: true
|
||||
});
|
||||
|
||||
for (var i=0;i<this.deviceList.length;i++) {
|
||||
var device = this.deviceList[i];
|
||||
$("#node-config-input-devices").editableList('addItem', device);
|
||||
}
|
||||
var listHeight = $("#node-config-input-devices").editableList('items').size() * 51 + 50;
|
||||
$("#node-config-input-devices").editableList('height', listHeight);
|
||||
},
|
||||
oneditsave: function() {
|
||||
var devices = $("#node-config-input-devices").editableList('items');
|
||||
var node = this;
|
||||
RED.nodes.eachNode(function(tmpNode) {
|
||||
if(tmpNode.type.indexOf("xiaomi-gateway") === 0 && tmpNode.gateway == node.id) {
|
||||
tmpNode.ip = $("#node-config-input-ip").val();
|
||||
tmpNode.changed = true;
|
||||
}
|
||||
});
|
||||
var devicesArray = [];
|
||||
devices.each(function(i) {
|
||||
var deviceElement = $(this);
|
||||
var sid = deviceElement.find("#node-config-input-sid-"+i).val();
|
||||
var desc = deviceElement.find("#node-config-input-desc-"+i).val();
|
||||
var model = deviceElement.find("#node-config-input-sid-"+i).typedInput('type');
|
||||
var d = {};
|
||||
d['sid']=sid;
|
||||
d['desc']=desc;
|
||||
d['model']=model;
|
||||
devicesArray.push(d);
|
||||
});
|
||||
node.deviceList = devicesArray;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="xiaomi-configurator">
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-config-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-ip"><i class="icon-tag"></i> IP address (v4 or v6)</label>
|
||||
<input type="text" id="node-config-input-ip" placeholder="IP">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-sid"><i class="icon-tag"></i> SID (optional)</label>
|
||||
<input type="text" id="node-config-input-sid" placeholder="sid">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-key"><i class="fa fa-ticket"></i> Key/Password</label>
|
||||
<input type="text" id="node-config-input-key" placeholder="Key">
|
||||
</div>
|
||||
<div class="form-row node-config-input-devices">
|
||||
<ol id="node-config-input-devices"></ol>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xiaomi-configurator">
|
||||
<p>Device configuration for Xiaomi nodes.</p>
|
||||
<h3>Details</h3>
|
||||
<p>This configuration node is used by the Xiaomi device nodes. Here you can add
|
||||
devices with their device-id (SID), type and a description.</p>
|
||||
<p>At the moment the following devices are supported:
|
||||
<lu>
|
||||
<li>Humidity & Temperature sensor [sensor ht/]</li>
|
||||
<li>Natgas sensor [natgas]</li>
|
||||
<li>Smoke sensor [smoke]</li>
|
||||
<li>Body motion sensor [motion]</li>
|
||||
<li>Magnet contact sensor [contact]</li>
|
||||
<li>Wall socket plug (zigbee) [plug]</li>
|
||||
<li>Push button [switch]</li>
|
||||
</lu>
|
||||
</p>
|
||||
<p>To be able to receive messages from the Xiaomi gateway, you need to set the gateway
|
||||
in developer mode. Once in developer mode, the gateway sends JSON messages over the network as
|
||||
UDP packages. On the internet their are a lot of guides on how to put the gateway in developer mode.</p>
|
||||
<p>If you want to use the wall sockets, you need to set the key from the gateway. The key can be
|
||||
retrieved via the Xiaomi Home App when in developer mode. Enter the key here and it is used
|
||||
together with the token from the gateway's heartbeat message to recalculate the key to switch
|
||||
the plug. If you do not specify a key, the plug-node can not be used.</p>
|
||||
|
||||
</script>
|
||||
14
node-red-contrib-xiaomi-configurator/xiaomi-configurator.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = (RED) => {
|
||||
function XiaomiConfiguratorNode(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
this.name = n.name;
|
||||
this.deviceList = n.deviceList || [];
|
||||
this.key = n.key;
|
||||
this.ip = n.ip;
|
||||
this.sid = this.sid || n.sid;
|
||||
|
||||
var node = this;
|
||||
}
|
||||
|
||||
RED.nodes.registerType("xiaomi-configurator", XiaomiConfiguratorNode);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
172
node-red-contrib-xiaomi-gateway/xiaomi-gateway.html
Normal file
@@ -0,0 +1,172 @@
|
||||
<!-- The Input Node -->
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('xiaomi-gateway', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"xiaomi-configurator"},
|
||||
name: {value: ""}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
outputLabels: ["Gateway"],
|
||||
paletteLabel: "gateway",
|
||||
icon: "mijia.png",
|
||||
label: function () {
|
||||
return this.name || "xiaomi-gateway";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="xiaomi-gateway">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xiaomi-gateway">
|
||||
<p>The gateway itself.</p>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<li>Devices output
|
||||
<dl class="message-properties">
|
||||
<dt>gateway <span class="property-type">xiaomi-configurator</span></dt>
|
||||
<dd>The gateway.</dd>
|
||||
</dl>
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">json</span></dt>
|
||||
<dd>Data from gateway, with computed data.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ol>
|
||||
</script>
|
||||
|
||||
<!-- The Input Node -->
|
||||
<script type="text/x-red" data-template-name="xiaomi-gateway in">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-ip"><i class="icon-tag"></i> IP</label>
|
||||
<input type="text" id="node-input-ip" placeholder="IP" readonly>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xiaomi-gateway in">
|
||||
<p>A Xiaomi Gateway input node, that produces a <code>msg.payload</code> containing a
|
||||
string with the gateway message content.
|
||||
</p>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('xiaomi-gateway in',{
|
||||
category: 'xiaomi in out',
|
||||
color: '#087F8A',
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
gateway: {value:"", type:"xiaomi-configurator"},
|
||||
ip: {value:""}
|
||||
},
|
||||
inputs:0,
|
||||
outputs:1,
|
||||
paletteLabel: "gateway in",
|
||||
icon: "mijia-io.png",
|
||||
label: function() {
|
||||
return this.name||"xiaomi-gateway";
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name?"node_label_italic":"";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
function changeGateway() {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val(configNode.name);
|
||||
}
|
||||
$('#node-input-ip').val(configNode.ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<!-- The Output Node -->
|
||||
<script type="text/x-red" data-template-name="xiaomi-gateway out">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-ip"><i class="icon-tag"></i> IP</label>
|
||||
<input type="text" id="node-input-ip" placeholder="IP" readonly>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xiaomi-gateway out">
|
||||
<p>This node sends <code>msg.payload</code> to the configured Xiaomi Gateway.</p>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('xiaomi-gateway out',{
|
||||
category: 'xiaomi in out',
|
||||
color: '#087F8A',
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
gateway: {value:"", type:"xiaomi-configurator"},
|
||||
ip: {value:""}
|
||||
},
|
||||
inputs:1,
|
||||
outputs:0,
|
||||
paletteLabel: "gateway out",
|
||||
icon: "mijia-io.png",
|
||||
align: "right",
|
||||
label: function() {
|
||||
return this.name||"xiaomi-gateway";
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name?"node_label_italic":"";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
function changeGateway() {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val(configNode.name);
|
||||
}
|
||||
$('#node-input-ip').val(configNode.ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
229
node-red-contrib-xiaomi-gateway/xiaomi-gateway.js
Normal file
@@ -0,0 +1,229 @@
|
||||
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);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
122
node-red-contrib-xiaomi-ht/xiaomi-ht.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('xiaomi-ht', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"xiaomi-configurator"},
|
||||
name: {value: ""},
|
||||
sid: {value: "", required: true}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
paletteLabel: "sensor HT",
|
||||
icon: "thermometer-icon.png",
|
||||
label: function () {
|
||||
return this.name || "xiaomi-ht";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
if(node.sid) {
|
||||
$('#node-input-sid').val(node.sid);
|
||||
}
|
||||
|
||||
function changeGateway(model) {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
$('#node-input-sid').empty();
|
||||
for (key in configNode.deviceList) {
|
||||
var device = configNode.deviceList[key];
|
||||
if (device.model === model) {
|
||||
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + '</option>');
|
||||
}
|
||||
}
|
||||
if(node.sid) {
|
||||
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-sid").change(function () {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val($('#node-input-sid option:selected').text());
|
||||
}
|
||||
});
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway("sensor_ht");
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
node.sid = $("#node-input-sid").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="xiaomi-ht">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
|
||||
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xiaomi-ht">
|
||||
<p>The Xiaomi Humidity & Temperature sensor node</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>
|
||||
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
|
||||
<hr>
|
||||
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
|
||||
<hr>
|
||||
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">object</span></dt>
|
||||
<dd>Data from gateway when used as a filter (see below).</dd>
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device SID.</dd>
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
|
||||
</dl>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>Sample payload from Aqara (which brings pressure):</p>
|
||||
<p><pre>{
|
||||
cmd: "read_ack"
|
||||
model: "weather.v1"
|
||||
sid: "158d00010b7f1b"
|
||||
short_id: 8451
|
||||
data: {
|
||||
voltage:3005,
|
||||
temperature:23.25,
|
||||
humidity:56.99,
|
||||
pressure:981.26,
|
||||
batteryLevel: 34
|
||||
}
|
||||
}</pre>
|
||||
Where <code>humidy</code> is in percents, <code>pressure</code> in hPa, <code>batteryLevel</code> is a computed percentage of remaining battery.
|
||||
</p>
|
||||
|
||||
</script>
|
||||
41
node-red-contrib-xiaomi-ht/xiaomi-ht.js
Normal file
@@ -0,0 +1,41 @@
|
||||
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);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
120
node-red-contrib-xiaomi-magnet/xiaomi-magnet.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('xiaomi-magnet', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"xiaomi-configurator"},
|
||||
name: {value: ""},
|
||||
sid: {value: "", required: true}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
paletteLabel: "contact",
|
||||
icon: "door-icon.png",
|
||||
label: function () {
|
||||
return this.name || "xiaomi-magnet";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
if(node.sid) {
|
||||
$('#node-input-sid').val(node.sid);
|
||||
}
|
||||
|
||||
function changeGateway(model) {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
$('#node-input-sid').empty();
|
||||
for (key in configNode.deviceList) {
|
||||
var device = configNode.deviceList[key];
|
||||
if (device.model === model) {
|
||||
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + '</option>');
|
||||
}
|
||||
}
|
||||
if(node.sid) {
|
||||
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-sid").change(function () {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val($('#node-input-sid option:selected').text());
|
||||
}
|
||||
});
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway("magnet");
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
node.sid = $("#node-input-sid").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="xiaomi-magnet">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
|
||||
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xiaomi-magnet">
|
||||
<p>The Xiaomi contact sensor node</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>
|
||||
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
|
||||
<hr>
|
||||
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
|
||||
<hr>
|
||||
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">object</span></dt>
|
||||
<dd>Data from gateway when used as a filter (see below).</dd>
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device SID.</dd>
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
|
||||
</dl>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>Sample message:</p>
|
||||
<p><pre>{
|
||||
cmd: "read_ack"
|
||||
model: "sensor_magnet.aq2"
|
||||
sid: "158d000112fb5d"
|
||||
short_id: 50301
|
||||
data: {
|
||||
voltage: 3015,
|
||||
status: "close",
|
||||
batteryLevel: 23
|
||||
}
|
||||
}</pre>
|
||||
Where <code>status</code> can be <code>"open"</code> or <code>"close"</code>, <code>batteryLevel</code> is a computed percentage of remaining battery.
|
||||
</p>
|
||||
|
||||
</script>
|
||||
9
node-red-contrib-xiaomi-magnet/xiaomi-magnet.js
Normal file
@@ -0,0 +1,9 @@
|
||||
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);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
122
node-red-contrib-xiaomi-motion/xiaomi-motion.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('xiaomi-motion', {
|
||||
category: 'xiaomi',
|
||||
color: '#3FADB5',
|
||||
defaults: {
|
||||
gateway: {value:"", type:"xiaomi-configurator"},
|
||||
name: {value: ""},
|
||||
sid: {value: "", required: true},
|
||||
motionmsg: {value: ""},
|
||||
nomotionmsg: {value: ""},
|
||||
output: {value: "0"}
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
paletteLabel: "motion",
|
||||
icon: "motion-icon.png",
|
||||
label: function () {
|
||||
return this.name || "xiaomi-motion";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
var node = this;
|
||||
|
||||
if(node.sid) {
|
||||
$('#node-input-sid').val(node.sid);
|
||||
}
|
||||
|
||||
function changeGateway(model) {
|
||||
var configNodeID = $('#node-input-gateway').val();
|
||||
if (configNodeID) {
|
||||
var configNode = RED.nodes.node(configNodeID);
|
||||
if(configNode) {
|
||||
$('#node-input-sid').empty();
|
||||
for (key in configNode.deviceList) {
|
||||
var device = configNode.deviceList[key];
|
||||
if (device.model === model) {
|
||||
$('#node-input-sid').append('<option value="' + device.sid + '">' + device.desc + '</option>');
|
||||
}
|
||||
}
|
||||
if(node.sid) {
|
||||
$('#node-input-sid option[value="' + node.sid + '"]').prop('selected', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#node-input-sid").change(function () {
|
||||
if(!this.name) {
|
||||
$("#node-input-name").val($('#node-input-sid option:selected').text());
|
||||
}
|
||||
});
|
||||
$("#node-input-gateway").change(function () {
|
||||
changeGateway("motion");
|
||||
});
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
node.sid = $("#node-input-sid").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="xiaomi-motion">
|
||||
<div class="form-row">
|
||||
<label for="node-input-gateway"><i class="icon-tag"></i> Gateway</label>
|
||||
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-sid"><i class="icon-tag"></i> Device</label>
|
||||
<select id="node-input-sid" placeholder="xiaomi gateway"></select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xiaomi-motion">
|
||||
<p>The Xiaomi body motion sensor node</p>
|
||||
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>payload
|
||||
<span class="property-type">object</span>
|
||||
</dt>
|
||||
<dd>
|
||||
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
|
||||
<hr>
|
||||
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
|
||||
<hr>
|
||||
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<ol class="node-ports">
|
||||
<dl class="message-properties">
|
||||
<dt>payload <span class="property-type">object</span></dt>
|
||||
<dd>Data from gateway when used as a filter (see below).</dd>
|
||||
<dt>sid <span class="property-type">string</span></dt>
|
||||
<dd>Device SID.</dd>
|
||||
<dt>gateway <span class="property-type">object</span></dt>
|
||||
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
|
||||
</dl>
|
||||
</ol>
|
||||
|
||||
<h4>Details</h4>
|
||||
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
|
||||
<p>Sample message:</p>
|
||||
<p><pre>{
|
||||
cmd: "read_ack"
|
||||
model: "motion"
|
||||
sid: "158d00015ef56c"
|
||||
short_id: 21672
|
||||
data: {
|
||||
voltage: 3035,
|
||||
status: "motion",
|
||||
batteryLevel: 45
|
||||
}
|
||||
}</pre>
|
||||
Where <code>batteryLevel</code> is a computed percentage of remaining battery.
|
||||
</p>
|
||||
</script>
|
||||