20 Commits

Author SHA1 Message Date
338419dca7 test 2026-02-28 21:58:16 +01:00
5183879654 upd 2026-02-28 21:40:58 +01:00
524e691645 typescript 2026-02-28 21:31:07 +01:00
0fccbca7b2 upd 2026-02-28 21:23:04 +01:00
c24007e42c upd 2026-02-28 21:17:22 +01:00
7698040f1c cleanup 2026-02-21 21:08:06 +01:00
e404c9a7ab mattes-seins 2026-02-21 20:41:14 +01:00
Pierre CLÉMENT
4c8978decf Update README.md 2018-04-17 21:52:46 +02:00
Pierre CLEMENT
627946c3d8 chore(distrib): version bump 2018-04-17 12:56:12 +02:00
Pierre CLÉMENT
7f033a442b Merge pull request #38 from readeral/xiaomi-actions-get-id-patch
Xiaomi actions get id patch
2018-04-17 12:54:09 +02:00
Pierre CLÉMENT
b984f415d5 Merge pull request #37 from readeral/update_docs
Update README.md
2018-04-17 12:53:52 +02:00
Pierre CLÉMENT
aeb6fcd047 Merge pull request #34 from artemsuv/fix_issue33
fix(socket) miDevicesUtils is not defined
2018-04-17 12:53:11 +02:00
Pierre CLÉMENT
4e3c3055d7 Merge pull request #35 from artemsuv/fix_data_not_defined
fix(socket) fix ReferenceError: data is not define
2018-04-17 12:52:54 +02:00
readeral
ec82e3882c Update xiaomi-actions.js 2018-02-13 13:32:10 +11:00
readeral
7880cc4f74 Update README.md 2018-02-12 23:33:34 +11:00
Artem Suvorov
a07eef88ae fix(socket) fix ReferenceError: data is not define 2018-02-04 00:11:15 +02:00
Artem Suvorov
af67f19217 fix(socket) miDevicesUtils is not defined 2018-02-03 23:16:25 +02:00
Pierre CLEMENT
e37c4aa71c Merge branch 'development' 2018-01-20 13:24:59 +01:00
Pierre CLEMENT
600f8d859c Merge branch 'master' of github.com:pierrecle/node-red-contrib-mi-devices 2018-01-11 00:44:08 +01:00
Pierre CLEMENT
6a9863814b feat(devices): handle yeelight basic support
Delete gateway in action config.
Close #4, close #8 and close #9
2018-01-11 00:43:29 +01:00
175 changed files with 2063 additions and 4894 deletions

36
.drone.yml Normal file
View 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
View File

@@ -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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
lts/*

18
.woodpecker/pipeline.yml Normal file
View 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

View File

@@ -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")):
![Mi devices example in node-red](resources/mi-devices-sample.png?raw=true "Mi devices flow sample")
### 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

View File

@@ -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;

View File

@@ -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;

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"));

View File

@@ -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;

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -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>

View File

@@ -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
View File

@@ -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
View File

@@ -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"));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1016 B

After

Width:  |  Height:  |  Size: 1016 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 601 B

After

Width:  |  Height:  |  Size: 601 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 990 B

After

Width:  |  Height:  |  Size: 990 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,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>

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

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -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>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View 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>

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

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View 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>

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

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View 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>

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

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View 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>

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

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View 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>

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