2
0

doc(main): update documentation and flows

This commit is contained in:
Pierre CLEMENT
2018-03-21 23:29:36 +01:00
parent c850ca9536
commit d04f5c1196
65 changed files with 2990 additions and 4 deletions

113
dist/devices/gateway/Gateway.js vendored Normal file
View File

@@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events = require("events");
const crypto = require("crypto");
const GatewayServer_1 = require("./GatewayServer");
const _1 = require("./");
const Color_1 = require("../../utils/Color");
class Gateway extends events.EventEmitter {
constructor(sid, ip) {
super();
this.sid = sid;
this.ip = ip;
this._subdevices = {};
}
set password(password) {
this._password = password;
}
get key() {
if (!this.lastToken || !this._password)
return null;
var cipher = crypto.createCipheriv('aes-128-cbc', this._password, Gateway.iv);
var key = cipher.update(Buffer.from(this.lastToken), "ascii", "hex");
cipher.final('hex');
return key;
}
handleMessage(msg) {
if (msg.data) {
if (msg.model === "gateway" && msg.sid === this.sid && msg.token) {
this.lastToken = msg.token;
}
}
if (msg.isGetIdListAck()) {
msg.data.forEach((sid) => {
this.read(sid);
});
}
if (msg.isReadAck() || msg.isReport()) {
if (!this._subdevices[msg.sid]) {
for (let SubDeviceClass of [_1.Magnet, _1.Motion, _1.Switch, _1.Weather]) {
if (SubDeviceClass.acceptedModels.indexOf(msg.model) >= 0) {
this._subdevices[msg.sid] = new SubDeviceClass(msg.sid, msg.model);
this._subdevices[msg.sid].on('values-updated', (sidOrMessage) => {
this.emit("subdevice-values-updated", sidOrMessage);
});
this.emit("subdevice-found", msg.sid);
}
}
}
if (this._subdevices[msg.sid]) {
this._subdevices[msg.sid].handleMessage(msg);
}
}
}
getSubdevice(sid) {
return this._subdevices[sid] || null;
}
hasSubdevice(sid) {
return !!this._subdevices[sid];
}
getIdList() {
this.send({
cmd: "get_id_list",
});
}
read(sid) {
this.send({ cmd: "read", sid: sid || this.sid });
}
setLight(brightness, rgb) {
this.send({
cmd: "write",
data: {
rgb: Color_1.Color.toValue(rgb.red, rgb.green, rgb.blue, brightness),
sid: this.sid
}
});
}
playSound(musicId, volume) {
this.send({
cmd: "write",
data: {
mid: musicId,
volume: volume,
sid: this.sid
}
});
}
send(message) {
let msg = Object.assign({}, message.payload || message);
if (msg.cmd) {
msg.sid = message.sid || this.sid;
if (msg.gateway) {
delete msg.gateway;
}
if (msg.cmd === "write") {
msg.data.key = this.key;
}
GatewayServer_1.GatewayServer.getInstance().sendToGateway(this.sid, msg);
}
}
get subdevices() {
return this._subdevices;
}
toJSON() {
return {
sid: this.sid,
ip: this.ip,
key: this.password,
subdevices: this.subdevices
};
}
}
Gateway.iv = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
exports.Gateway = Gateway;

30
dist/devices/gateway/GatewayMessage.js vendored Normal file
View File

@@ -0,0 +1,30 @@
"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

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

121
dist/devices/gateway/GatewayServer.js vendored Normal file
View File

@@ -0,0 +1,121 @@
"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

@@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events = require("events");
class GatewaySubdevice extends events.EventEmitter {
constructor(sid, model) {
super();
this.sid = sid;
this.model = model;
}
get batteryLevel() {
/*
When full, CR2032 batteries are between 3 and 3.4V
http://farnell.com/datasheets/1496885.pdf
*/
return this.voltage ? Math.min(Math.round((this.voltage - 2200) / 10), 100) : -1;
}
handleMessage(msg) {
if (msg.data.voltage) {
this.voltage = msg.data.voltage;
}
this.message = msg;
}
static get acceptedModels() {
return [];
}
;
toJSON() {
let json = {};
for (let prop of Object.keys(this)) {
json[prop] = this[prop];
}
delete json._events;
delete json._eventsCount;
delete json._maxListeners;
json.batteryLevel = this.batteryLevel;
json.internalModel = this.internalModel;
return json;
}
}
exports.GatewaySubdevice = GatewaySubdevice;

32
dist/devices/gateway/Magnet.js vendored Normal file
View File

@@ -0,0 +1,32 @@
"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;
// mintime
if (this.status !== data.status) {
this.status = data.status;
this.emit('values-updated', this.sid);
}
}
}
}
exports.Magnet = Magnet;

29
dist/devices/gateway/Motion.js vendored Normal file
View File

@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const GatewaySubdevice_1 = require("./GatewaySubdevice");
class Motion extends GatewaySubdevice_1.GatewaySubdevice {
constructor() {
super(...arguments);
this.lux = 0;
}
static get acceptedModels() {
return ['motion', 'sensor_motion.aq2'];
}
get internalModel() {
return 'mi.motion';
}
handleMessage(msg) {
super.handleMessage(msg);
if (msg.isReadAck() || msg.isReport()) {
let data = msg.data;
if (data.lux) {
this.lux = parseInt(data.lux);
}
if (data.status === "motion") {
this.lastMotionTimestamp = data.timestamp;
this.emit('values-updated', { sid: this.sid, data: { hasMotion: true } });
}
}
}
}
exports.Motion = Motion;

15
dist/devices/gateway/Switch.js vendored Normal file
View File

@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const GatewaySubdevice_1 = require("./GatewaySubdevice");
class Switch extends GatewaySubdevice_1.GatewaySubdevice {
static get acceptedModels() {
return ['switch', 'sensor_switch.aq2'];
}
get internalModel() {
return 'mi.switch';
}
handleMessage(msg) {
super.handleMessage(msg);
}
}
exports.Switch = Switch;

36
dist/devices/gateway/Weather.js vendored Normal file
View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const GatewaySubdevice_1 = require("./GatewaySubdevice");
class Weather extends GatewaySubdevice_1.GatewaySubdevice {
static get acceptedModels() {
return ['sensor_ht', 'weather.v1'];
}
get internalModel() {
return 'mi.weather';
}
get temperatureInDegrees() {
return this.temperature / 100;
}
get humidityInPercent() {
return this.humidity / 100;
}
get pressureInBar() {
return this.pressure / 100000;
}
get pressureInhPa() {
return this.pressure / 100;
}
handleMessage(msg) {
super.handleMessage(msg);
if (msg.isReadAck() || msg.isReport()) {
let data = msg.data;
['temperature', 'humidity', 'pressure'].forEach((dataType) => {
if (data[dataType]) {
this[dataType] = parseInt(data[dataType]);
}
});
this.emit('values-updated', this.sid);
}
}
}
exports.Weather = Weather;

14
dist/devices/gateway/index.js vendored Normal file
View File

@@ -0,0 +1,14 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./"));
__export(require("./Gateway"));
__export(require("./GatewayMessage"));
__export(require("./GatewayServer"));
__export(require("./GatewaySubdevice"));
__export(require("./Magnet"));
__export(require("./Motion"));
__export(require("./Switch"));
__export(require("./Weather"));

45
dist/devices/yeelight/YeelightServer.js vendored Normal file
View File

@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events = require("events");
const YeelightSearch = require("yeelight-wifi");
class YeelightServer extends events.EventEmitter {
constructor() {
super(...arguments);
this._bulbs = {};
this._bulbsJson = {};
}
static getInstance() {
if (!this.instance) {
this.instance = new YeelightServer();
}
return this.instance;
}
get bulbs() {
return this._bulbsJson;
}
getBulb(sid) {
return this._bulbs[sid];
}
discover() {
new Promise(() => {
(new YeelightSearch()).on('found', (bulb) => {
bulb.sid = parseInt(bulb.id);
if (!this._bulbs[bulb.sid]) {
this._bulbs[bulb.sid] = bulb;
this._bulbsJson[bulb.sid] = YeelightServer.bulbToJSON(bulb);
this.emit("yeelight-online", bulb.sid);
}
});
});
// TODO: disconected ?
}
static bulbToJSON(bulb) {
return {
sid: bulb.sid,
ip: bulb.hostname,
name: bulb.name,
model: bulb.model
};
}
}
exports.YeelightServer = YeelightServer;

0
dist/devices/yeelight/index.js vendored Normal file
View File

26
dist/nodes/actions/GatewayPlaySound.js vendored Normal file
View File

@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class GatewayPlaySound {
constructor(props) {
RED.nodes.createNode(this, props);
this.mid = parseInt(props.mid);
this.volume = parseInt(props.volume);
this.setListeners();
}
setListeners() {
this.on('input', (msg) => {
if (msg.sid) {
msg.payload = {
action: "playSound",
mid: msg.mid || this.mid,
volume: msg.volume || this.volume
};
}
this.send(msg);
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions gateway_play_sound`, GatewayPlaySound);
};

23
dist/nodes/actions/GatewayStopSound.js vendored Normal file
View File

@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class GatewayStopSound {
constructor(props) {
RED.nodes.createNode(this, props);
this.setListeners();
}
setListeners() {
this.on('input', (msg) => {
if (msg.sid) {
msg.payload = {
action: "playSound",
mid: 1000
};
}
this.send(msg);
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions gateway_stop_sound`, GatewayStopSound);
};

24
dist/nodes/actions/Light.js vendored Normal file
View File

@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class Light {
constructor(props) {
RED.nodes.createNode(this, props);
this.color = props.color;
this.brightness = props.brightness;
this.setListeners();
}
setListeners() {
this.on('input', (msg) => {
msg.payload = {
action: "setLight",
color: msg.color || this.color,
brightness: msg.brightness || this.brightness
};
this.send(msg);
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions light`, Light);
};

20
dist/nodes/actions/ReadAction.js vendored Normal file
View File

@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED, type) => {
class ReadAction {
constructor(props) {
RED.nodes.createNode(this, props);
this.on('input', (msg) => {
if (msg.sid) {
msg.payload = {
cmd: this.type.replace(`${constants_1.Constants.NODES_PREFIX}-actions `, ''),
sid: msg.sid
};
this.send(msg);
}
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions ${type}`, ReadAction);
};

18
dist/nodes/actions/ToggleAction.js vendored Normal file
View File

@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED, action) => {
class ToggleAction {
constructor(props) {
RED.nodes.createNode(this, props);
this.setListeners();
}
setListeners() {
this.on('input', (msg) => {
msg.payload = { action };
this.send(msg);
});
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-actions ${action}`, ToggleAction);
};

23
dist/nodes/actions/WriteAction.js vendored Normal file
View File

@@ -0,0 +1,23 @@
"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);
};

BIN
dist/nodes/actions/icons/mi-bulb.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

BIN
dist/nodes/actions/icons/mi-click.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
dist/nodes/actions/icons/mi-list.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

BIN
dist/nodes/actions/icons/mi-mute.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
dist/nodes/actions/icons/mi-off.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
dist/nodes/actions/icons/mi-on.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
dist/nodes/actions/icons/mi-read.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

BIN
dist/nodes/actions/icons/mi-sound.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
dist/nodes/actions/icons/mi-toggle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

483
dist/nodes/actions/index.html vendored Normal file
View File

@@ -0,0 +1,483 @@
<script type="text/x-red" data-template-name="mi-devices-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">
<p>Ask the gateway to read the report of the input device.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<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>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions read',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "read",
icon: "mi-read.png",
label: function() {
return this.name||"mi-devices read";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-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">
<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>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions get_id_list',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "get id list",
icon: "mi-list.png",
label: function() {
return this.name||"mi-devices get id list";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-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">
<p>Virtual single click for switch.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">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>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions click',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "click",
icon: "mi-click.png",
label: function() {
return this.name||"mi-devices click";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-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">
<p>Virtual double click for switch.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>sid <span class="property-type">string</span></dt>
<dd>Device <code>sid</code> to ask the report.</dd>
<dt>gateway <span class="property-type">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>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions double_click',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "double click",
icon: "double-click.png",
label: function() {
return this.name||"mi-devices double click";
}
});
</script>
<!-- The Gateway light Node -->
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions light', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value: ""},
brightness: {value: 100},
hexRgbColor: {value: "#ffffff"},
color: {value:{red: 255, green: 255, blue: 255}}
},
inputs: 1,
outputs: 1,
paletteLabel: "set light",
icon: "mi-bulb.png",
label: function () {
return this.name || "set light";
},
oneditsave: function() {
var hexRgbColor = $("#node-input-hexRgbColor").val();
var split = hexRgbColor.slice(1).match(/.{1,2}/g).map(function(hexColor) {
return parseInt(hexColor, 16);
});
this.color = {
red: split[0],
green: split[1],
blue: split[2]
};
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-actions light">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-brightness"><i class="icon-tag"></i> Brightness</label>
<input type="range" id="node-input-brightness" min="0" max="100">
</div>
<div class="form-row">
<label for="node-input-hexRgbColor"><i class="icon-tag"></i> Color</label>
<input type="color" id="node-input-hexRgbColor">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-actions light">
<p>Change the light of the gateway.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>brightness
<span class="property-type">number</span>
</dt>
<dd>The brightness value between <code>0</code> and <code>100</code>.</dd>
<dt>color
<span class="property-type">object</span>
</dt>
<dd>The color itself. This object must contain the followinf properties:
<ul>
<li>
<code>red</code> - amout of red, between <code>0</code> and <code>255</code>
</li>
<li>
<code>green</code> - amout of green, between <code>0</code> and <code>255</code>
</li>
<li>
<code>blue</code> - amout of blue, between <code>0</code> and <code>255</code>
</li>
</ul>
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway out node.</li>
</ol>
</script>
<!-- The Gateway sound Node -->
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions gateway_play_sound', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value: ""},
mid: {value: ""},
volume: {value: ""}
},
inputs: 1,
outputs: 1,
paletteLabel: "play sound",
icon: "mi-sound.png",
label: function () {
return this.name || "play sound";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-actions gateway_play_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>
<div class="form-row">
<label for="node-input-mid"><i class="icon-tag"></i> Sound</label>
<select id="node-input-mid">
<option value="0">Police car tone 1</option>
<option value="1">Police car tone 2</option>
<option value="2">Safety incident sound</option>
<option value="3">Missile countdown</option>
<option value="4">Ghost cry</option>
<option value="5">Sniper rifle</option>
<option value="6">Battle sound</option>
<option value="7">Air raid alarm</option>
<option value="8">Barking</option>
<option value="10">Doorbell tone</option>
<option value="11">Door knocking tone</option>
<option value="12">Funny tone</option>
<option value="13">Alarm clock tone</option>
<option value="20">MiMix</option>
<option value="21">Enthusisatic</option>
<option value="22">GuitarClassic</option>
<option value="23">IceWorldPiano</option>
<option value="24">LeisureTime</option>
<option value="25">ChildHood</option>
<option value="26">MorningStreamLet</option>
<option value="27">MusicBox</option>
<option value="28">Orange</option>
<option value="29">Thinker</option>
</select>
</div>
<div class="form-row">
<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">
<p>Play a sound on the gateway.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>mid
<span class="property-type">number</span>
</dt>
<dd>Music ID (user define sounds start from <code>1001</code>, <code>1000</code> means stop).</dd>
<dt>volume
<span class="property-type">number</span>
</dt>
<dd>Playing volume, between <code>0</code> and <code>100</code>.</dd>
</dl>
<h3>Outputs</h3>
<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', {
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value: ""}
},
inputs: 1,
outputs: 1,
paletteLabel: "stop sound",
icon: "mi-mute.png",
label: function () {
return this.name || "stop sound";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-actions gateway_stop_sound">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-actions gateway_stop_sound">
<p>
Stop current playing sound on the gateway.
</p>
<h3>Outputs</h3>
<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>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions turn_on',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "turn on",
icon: "mi-on.png",
label: function() {
return this.name||"mi-devices turn on";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-actions turn_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 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>
<h3>Outputs</h3>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions turn_off',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "turn off",
icon: "mi-off.png",
label: function() {
return this.name||"mi-devices turn off";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-actions toggle">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="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>
<h3>Outputs</h3>
<p class="node-ports">Message to connect to a gateway out node.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions toggle',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "toggle",
icon: "mi-toggle.png",
label: function() {
return this.name||"mi-devices toggle";
}
});
</script>

21
dist/nodes/actions/index.js vendored Normal file
View File

@@ -0,0 +1,21 @@
"use strict";
const ReadAction_1 = require("./ReadAction");
const WriteAction_1 = require("./WriteAction");
const Light_1 = require("./Light");
const GatewayPlaySound_1 = require("./GatewayPlaySound");
const GatewayStopSound_1 = require("./GatewayStopSound");
const ToggleAction_1 = require("./ToggleAction");
module.exports = (RED) => {
["read", "get_id_list"].forEach((action) => {
ReadAction_1.default(RED, action);
});
["click", "double_click"].forEach((action) => {
WriteAction_1.default(RED, action);
});
Light_1.default(RED);
GatewayPlaySound_1.default(RED);
GatewayStopSound_1.default(RED);
["turn_on", "turn_off", "toggle"].forEach(action => {
ToggleAction_1.default(RED, action);
});
};

10
dist/nodes/constants.js vendored Normal file
View File

@@ -0,0 +1,10 @@
"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;

59
dist/nodes/gateway-subdevices/All.js vendored Normal file
View File

@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
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.gateway = RED.nodes.getNode(props.gateway);
this.onlyModels = All.getOnlyModelsValue(props.onlyModels || []);
this.excludedSids = props.excludedSids;
this.setMessageListener();
}
setMessageListener() {
this.on('input', (msg) => {
if (this.gateway) {
// Filter input
if (msg.payload && msg.payload.model && msg.payload.sid) {
if (!this.isDeviceValid(msg.payload)) {
msg = null;
}
this.send(msg);
}
else {
Object.keys(this.gateway.deviceList || {})
.filter((sid) => this.isDeviceValid(sid))
.forEach((sid) => {
let curMsg = Object.assign({}, msg);
curMsg.sid = sid;
curMsg.gateway = this.gateway;
this.send(curMsg);
});
}
}
});
}
isDeviceValid(sid) {
if ((!this.onlyModels || this.onlyModels.length == 0) && (!this.excludedSids || this.excludedSids.length == 0)) {
return true;
}
let device = this.gateway.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

@@ -0,0 +1,45 @@
"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}-${type}`, GatewayDevice);
};

34
dist/nodes/gateway-subdevices/Plug.js vendored Normal file
View File

@@ -0,0 +1,34 @@
"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);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

729
dist/nodes/gateway-subdevices/index.html vendored Normal file
View File

@@ -0,0 +1,729 @@
<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-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 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-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-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>

11
dist/nodes/gateway-subdevices/index.js vendored Normal file
View File

@@ -0,0 +1,11 @@
"use strict";
const All_1 = require("./All");
const Plug_1 = require("./Plug");
const GatewaySubdevice_1 = require("./GatewaySubdevice");
module.exports = (RED) => {
All_1.default(RED);
Plug_1.default(RED);
["magnet", "motion", "sensor", "switch"].forEach((subdeviceType) => {
GatewaySubdevice_1.default(RED, subdeviceType);
});
};

46
dist/nodes/gateway/Gateway.js vendored Normal file
View File

@@ -0,0 +1,46 @@
"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

@@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const GatewayServer_1 = require("../../devices/gateway/GatewayServer");
exports.default = (RED) => {
class GatewayConfigurator {
constructor(props) {
RED.nodes.createNode(this, props);
let { sid, key, deviceList } = props;
this.sid = sid;
this.key = key;
this.deviceList = deviceList;
let server = GatewayServer_1.GatewayServer.getInstance();
if (this.sid) {
this.setGateway();
}
server.on('gateway-online', (sid) => {
if (sid === this.sid) {
this.setGateway();
this.emit('gateway-online');
}
});
server.on('gateway-offline', (sid) => {
if (sid === this.sid) {
this._gateway = null;
this.emit('gateway-offline');
}
});
}
setGateway() {
this._gateway = GatewayServer_1.GatewayServer.getInstance().getGateway(this.sid);
if (this._gateway) {
this._gateway.password = this.key;
this._gateway.on("subdevice-values-updated", (sidOrMessage) => {
let sid = sidOrMessage.sid || sidOrMessage;
let subdevice = this._gateway.getSubdevice(sid);
if (subdevice) {
(sidOrMessage.data ? Object.keys(sidOrMessage.data) : []).forEach((key) => {
subdevice[key] = sidOrMessage.data[key];
});
this.emit('subdevice-update', subdevice);
}
});
}
}
get gateway() {
return this._gateway;
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway configurator`, GatewayConfigurator, {
settings: {
miDevicesGatewayConfiguratorDiscoveredGateways: {
value: GatewayServer_1.GatewayServer.getInstance().gateways,
exportable: true
}
}
});
};

27
dist/nodes/gateway/GatewayIn.js vendored Normal file
View File

@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
exports.default = (RED) => {
class GatewayIn {
constructor(props) {
RED.nodes.createNode(this, props);
this.gatewayConf = RED.nodes.getNode(props.gateway);
this.status({ fill: "red", shape: "ring", text: "offline" });
if (this.gatewayConf.gateway) {
this.gatewayOnline();
}
this.gatewayConf.on('gateway-online', () => this.gatewayOnline());
this.gatewayConf.on('gateway-offline', () => this.gatewayOffline());
}
gatewayOnline() {
this.status({ fill: "blue", shape: "dot", text: "online" });
this.gatewayConf.on('subdevice-update', (subdevice) => {
this.send({ payload: subdevice });
});
}
gatewayOffline() {
this.status({ fill: "red", shape: "ring", text: "offline" });
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-gateway in`, GatewayIn);
};

35
dist/nodes/gateway/GatewayOut.js vendored Normal file
View File

@@ -0,0 +1,35 @@
"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);
};

BIN
dist/nodes/gateway/icons/mijia-io.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
dist/nodes/gateway/icons/mijia.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

295
dist/nodes/gateway/index.html vendored Normal file
View File

@@ -0,0 +1,295 @@
<script type="text/javascript">
RED.nodes.registerType('mi-devices-gateway configurator', {
category: 'config',
defaults: {
name: {value: ""},
sid: {value: ""},
key: { value: "" },
deviceList: {value:{}}
},
paletteLabel: "gateway configurator",
label: function () {
return this.name || "gateway configurator";
},
oneditprepare: function() {
var foundGateways = RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways;
Object.keys(foundGateways).forEach(function(sid) {
var gateway = foundGateways[sid];
$('#discovered-gateways').append('<option value="' + gateway.sid + '">' + gateway.sid + ' - ' + gateway.ip + '</option>');
});
var node = this;
var devicesConfig = {
"mi.weather": {label:"weather", icon:"icons/node-red-contrib-mi-devices/thermometer-icon.png"},
"mi.magnet": {label:"magnet", icon:"icons/node-red-contrib-mi-devices/door-icon.png"},
"mi.motion": {label:"motion", icon:"icons/node-red-contrib-mi-devices/motion-icon.png"},
"mi.switch": {label:"switch", icon:"icons/node-red-contrib-mi-devices/mi-switch.png"},
"mi.plug": {label:"plug zigbee", icon:"icons/node-red-contrib-mi-devices/outlet-icon.png"}
};
$("#node-config-input-subdevices").css('min-height','250px').css('min-width','450px').editableList({
addItem: function(container, i, device) {
var row = container;
$('<label/>',{for:"node-config-input-sid-"+i, style:"margin-left: 3px; width: 15px;vertical-align:middle"}).appendTo(row);
var sid = $('<input/>',{id:"node-config-input-sid-"+i,type:"text", placeholder:"SID", style:"width:auto;vertical-align:top"}).appendTo(row);
sid.typedInput({
default: 'mi.weather',
types: Object.keys(devicesConfig).map(function(type) {
var cleanType = devicesConfig[type];
cleanType.value = type;
return cleanType;
})
});
$('<label/>',{for:"node-config-input-desc-"+i, style:"margin-left: 7px; width: 20px;vertical-align:middle"}).html('<i class="fa fa-pencil-square-o"></i>').appendTo(row);
var desc = $('<input/>',{id:"node-config-input-desc-"+i, type:"text", placeholder:"name", style:"width:auto;vertical-align:top"}).appendTo(row);
sid.typedInput('value', device.sid);
sid.typedInput('type', device.internalModel);
desc.val(device.name);
container.parent().attr("data-sid", device.sid);
},
removeItem: function(device) {
$("#node-config-input-subdevices").find("[data-sid=" + device.sid + "]").remove();
},
sortable: false,
removable: true
});
$('#discovered-gateways').on('change', function() {
var sid = $('#discovered-gateways').val();
$('#input-subdevices > *').remove();
var gateway = sid && RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways[sid];
$("#node-config-input-sid").val(gateway && gateway.sid);
$("#node-config-input-key").val(gateway && gateway.key);
$("#node-config-input-subdevices").editableList('items').each(function(i, elt) {
$("#node-config-input-subdevices").editableList('removeItem', {sid: $(elt).find("#node-config-input-sid-"+i).val()});
});
var subdevices = gateway && Object.keys(gateway.subdevices).map(function(sid) { return gateway.subdevices[sid]; });
subdevices && subdevices.sort(function(a, b) { return a.internalModel > b.internalModel; }).forEach(function(device) {
if(!devicesConfig[device.internalModel] || !device.sid) {
return;
}
if(node.deviceList[device.sid]) {
device.name = node.deviceList[device.sid].name;
}
$("#node-config-input-subdevices").editableList('addItem', {
sid: device.sid,
internalModel: device.internalModel,
name: device.name
});
});
var listHeight = $("#node-config-input-subdevices").editableList('items').size() * 51 + 50;
$("#node-config-input-subdevices").editableList('height', listHeight);
});
Object.keys(this.deviceList || {}).forEach(function(sid) {
var device = node.deviceList[sid];
$("#node-config-input-subdevices").editableList('addItem', {
sid: sid,
internalModel: device.internalModel,
name: device.name
});
});
var listHeight = $("#node-config-input-subdevices").editableList('items').size() * 51 + 50;
$("#node-config-input-subdevices").editableList('height', listHeight);
},
oneditsave: function() {
var node = this;
var devices = $("#node-config-input-subdevices").editableList('items');
devices.each(function(i, elt) {
var deviceElement = $(elt);
var sid = deviceElement.find("#node-config-input-sid-"+i).val();
var desc = deviceElement.find("#node-config-input-desc-"+i).val();
var internalModel = deviceElement.find("#node-config-input-sid-"+i).typedInput('type');
node.deviceList[sid] = {internalModel: internalModel, name: desc};
});
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-gateway configurator">
<div class="form-row">
<label for="discovered-gateways"><i class="fa fa-search"></i> Found gateways</label>
<select id="discovered-gateways">
<option>- Select -</option>
</select>
</div>
<hr>
<h4>Gateway</h4>
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-config-input-sid"><i class="fa fa-barcode"></i> SID</label>
<input type="text" id="node-config-input-sid" placeholder="sid">
</div>
<div class="form-row">
<label for="node-input-gatewayKey"><i class="fa fa-key"></i> Key</label>
<input type="text" id="node-config-input-key" placeholder="Key">
</div>
<h4>Devices</h4>
<div class="form-row node-config-input-subdevices">
<ol id="node-config-input-subdevices"></ol>
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-gateway configurator">
<p>Gateway configuration for Xiaomi nodes.</p>
<h3>Details</h3>
<p>This configuration node is used by the Xiaomi device nodes. Here you can add
devices with their device-id (SID), type and a description.</p>
<p>At the moment the following devices are supported:
<lu>
<li>Humidity & Temperature sensor [sensor ht/]</li>
<li>Body motion sensor [motion]</li>
<li>Magnet contact sensor [contact]</li>
<li>Wall socket plug (zigbee) [plug]</li>
<li>Push button [switch]</li>
</lu>
</p>
<p>To be able to receive messages from the Xiaomi gateway, you need to set the gateway
in developer mode. Once in developer mode, the gateway sends JSON messages over the network as
UDP packages. On the internet their are a lot of guides on how to put the gateway in developer mode.</p>
<p>If you want to use the wall sockets, you need to set the key from the gateway. The key can be
retrieved via the Xiaomi Home App when in developer mode. Enter the key here and it is used
together with the token from the gateway's heartbeat message to recalculate the key to switch
the plug. If you do not specify a key, the plug-node can not be used.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-gateway', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"mi-devices-gateway configurator"},
name: {value: ""}
},
inputs: 1,
outputs: 1,
outputLabels: ["Gateway"],
paletteLabel: "gateway",
icon: "mijia.png",
label: function () {
return this.name || "gateway";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-gateway">
<div class="form-row">
<label for="node-input-gateway"><i class="fa fa-wrench"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-gateway">
<p>The gateway itself.</p>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Devices output
<dl class="message-properties">
<dt>gateway <span class="property-type">mi-devices-gateway configurator</span></dt>
<dd>The gateway.</dd>
</dl>
<dl class="message-properties">
<dt>payload <span class="property-type">json</span></dt>
<dd>Data from gateway, with computed data.</dd>
</dl>
</li>
</ol>
</script>
<script type="text/x-red" data-template-name="mi-devices-gateway in">
<div class="form-row">
<label for="node-input-gateway"><i class="fa fa-wrench"></i> Gateway</label>
<input type="text" id="node-input-gateway" placeholder="xiaomi gateway">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-gateway in">
<p>A Xiaomi Gateway input node, that produces a <code>msg.payload</code> containing a
string with the gateway message content.
</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-gateway in',{
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""},
gateway: {value:"", type:"mi-devices-gateway configurator"}
},
inputs:0,
outputs:1,
paletteLabel: "gateway in",
icon: "mijia-io.png",
label: function() {
return this.name||"gateway in";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
function changeGateway() {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
if(!this.name) {
$("#node-input-name").val(configNode.name);
}
$('#node-input-ip').val(configNode.ip);
}
}
}
$("#node-input-gateway").change(function () {
changeGateway();
});
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-gateway out">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-gateway out">
<p>This node sends <code>msg.payload</code> to <code>msg.gateway</code> Xiaomi Gateway.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-gateway out',{
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""}
},
inputs:1,
outputs:0,
paletteLabel: "gateway out",
icon: "mijia-io.png",
align: "right",
label: function() {
return this.name||"gateway out";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
</script>

13
dist/nodes/gateway/index.js vendored Normal file
View File

@@ -0,0 +1,13 @@
"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);
};

67
dist/nodes/plug-wifi/index.html vendored Normal file
View File

@@ -0,0 +1,67 @@
<script type="text/javascript">
RED.nodes.registerType('mi-devices-plug-wifi', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
name: {value: ""},
ip: {value: "", required: true},
onmsg: {value: ""},
offmsg: {value: ""},
output: {value: "0"}
},
inputs: 1,
outputs: 1,
outputLabels: ["Status"],
paletteLabel: "plug (wifi)",
icon: "outlet-wifi-icon.png",
label: function () {
return this.name || "plug-wifi";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-plug-wifi">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-ip"><i class="icon-tag"></i> Ip</label>
<input type="text" id="node-input-ip" placeholder="ip address">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-plug-wifi">
<p>The Xiaomi plug (wifi) node</p>
<p>This is the wiFi plug (socket). To control the Wifi-Plug, extensive use is made of the miio library created by <a href="https://github.com/aholstenson/miio">Andreas Holstenson</a>. Make sure to check his page for compatible devices.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">string</span>
</dt>
<dd><code>on</code> or <code>off</code>.</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway, see below.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>On the input you can send the string <code>on</code> to switch the plug on. To turn it off just send the string <code>off</code></p>
<p>Sample message full data:</p>
<p><pre>{
type: "power-plug",
model: "chuangmi.plug.m1",
capabilities: [ {"0": "power-channels"} ],
address: "192.168.178.31",
port: 54321,
power: { "0": false },
state: "on"
}</pre></p>
</script>

139
dist/nodes/plug-wifi/index.js vendored Normal file
View File

@@ -0,0 +1,139 @@
"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

@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const YeelightServer_1 = require("../../devices/yeelight/YeelightServer");
exports.default = (RED) => {
class YeelightConfigurator {
constructor(props) {
RED.nodes.createNode(this, props);
let { sid } = props;
this.sid = parseInt(sid);
if (this.sid) {
this.setBulb();
}
let server = YeelightServer_1.YeelightServer.getInstance();
server.on('yeelight-online', (sid) => {
if (sid === this.sid) {
this.setBulb();
this.emit('bulb-online');
}
});
server.on('yeelight-offline', (sid) => {
if (sid === this.sid) {
this._bulb = null;
this.emit('bulb-offline');
}
});
}
setBulb() {
this._bulb = YeelightServer_1.YeelightServer.getInstance().getBulb(this.sid);
}
get bulb() {
return this._bulb;
}
}
RED.nodes.registerType(`${constants_1.Constants.NODES_PREFIX}-yeelight configurator`, YeelightConfigurator, {
settings: {
miDevicesYeelightConfiguratorDiscoveredBulbs: {
value: YeelightServer_1.YeelightServer.getInstance().bulbs,
exportable: true
}
}
});
};

64
dist/nodes/yeelight/YeelightOut.js vendored Normal file
View File

@@ -0,0 +1,64 @@
"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.

After

Width:  |  Height:  |  Size: 1.5 KiB

160
dist/nodes/yeelight/index.html vendored Normal file
View File

@@ -0,0 +1,160 @@
<script type="text/javascript">
RED.nodes.registerType('mi-devices-yeelight configurator', {
category: 'config',
defaults: {
name: {value: ""},
sid: {value: ""}
},
label: function () {
return this.name || "yeelight conf";
},
oneditprepare: function() {
var foundBulbs = RED.settings.miDevicesYeelightConfiguratorDiscoveredBulbs;
Object.keys(foundBulbs).forEach(function(sid) {
var bulb = foundBulbs[sid];
$('#discovered-bulbs').append('<option value="' + bulb.sid + '">' + (bulb.name || bulb.sid) + ' - ' + bulb.model + ' - ' + bulb.ip + '</option>');
});
$('#discovered-bulbs').on('change', function() {
var sid = $('#discovered-bulbs').val();
var bulb = foundBulbs[sid];
$("#node-config-input-name").val(bulb && bulb.name);
$("#node-config-input-sid").val(bulb && bulb.sid);
});
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-yeelight configurator">
<div class="form-row">
<label for="discovered-bulbs"><i class="fa fa-search"></i> Found bulbs</label>
<select id="discovered-bulbs">
<option>- Select -</option>
</select>
</div>
<hr>
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-config-input-sid"><i class="fa fa-barcode"></i> SID</label>
<input type="text" id="node-config-input-sid" placeholder="sid">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-yeelight configurator">
<p>Xiaomi Yeelight configuration node.</p>
<h3>Details</h3>
<p>This configuration node is used by the Yeelight nodes. Here you can add
devices with their device-id (SID), type and a description.</p>
<p>At the moment the following devices are supported:
<lu>
<li>Humidity & Temperature sensor [sensor ht/]</li>
<li>Body motion sensor [motion]</li>
<li>Magnet contact sensor [contact]</li>
<li>Wall socket plug (zigbee) [plug]</li>
<li>Push button [switch]</li>
</lu>
</p>
<p>To be able to receive messages from the Xiaomi gateway, you need to set the gateway
in developer mode. Once in developer mode, the gateway sends JSON messages over the network as
UDP packages. On the internet their are a lot of guides on how to put the gateway in developer mode.</p>
<p>If you want to use the wall sockets, you need to set the key from the gateway. The key can be
retrieved via the Xiaomi Home App when in developer mode. Enter the key here and it is used
together with the token from the gateway's heartbeat message to recalculate the key to switch
the plug. If you do not specify a key, the plug-node can not be used.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('mi-devices-yeelight out', {
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value: ""},
yeelight: {value:"", type:"mi-devices-yeelight configurator"}
},
inputs: 1,
outputs: 0,
paletteLabel: "yeelight out",
icon: "mi-yeelight.png",
align: "right",
label: function () {
return this.name || "yeelight out";
},
oneditprepare: function() {
function changeGateway() {
var configNodeID = $('#node-input-gateway').val();
if (configNodeID) {
var configNode = RED.nodes.node(configNodeID);
if(configNode) {
if(!this.name) {
$("#node-input-name").val(configNode.name);
}
$('#node-input-ip').val(configNode.ip);
}
}
}
$("#node-input-gateway").change(function () {
changeGateway();
});
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-yeelight out">
<div class="form-row">
<label for="node-input-yeelight"><i class="fa fa-wrench"></i> Yeelight</label>
<input type="text" id="node-input-yeelight" placeholder="yeelight">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-yeelight out">
<p>The Xiaomi Yeelight node</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd>
When the message contains a <code>sid</code> field, the node will filter the input and output only if the <code>sid</code> is the device's sid.<br>
<hr>
If the message doesn't contain a <code>sid</code> field, the node will be used to inject <code>sid</code> and <code>gateway</code> fields in the incoming <code>msg</code>.<br>
<hr>
Input Gateway node produces message of type <code>read_ack</code>, <code>heartbeat</code> or <code>report</code>.
</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>Data from gateway when used as a filter (see below).</dd>
<dt>sid <span class="property-type">string</span></dt>
<dd>Device SID.</dd>
<dt>gateway <span class="property-type">object</span></dt>
<dd>The <code>xiaomi-configurator</code> object where the device is registred.</dd>
</dl>
</ol>
<h4>Details</h4>
<p>The incoming message is processed if the input <code>sid</code> matches the configured value for this device.</p>
<p>Sample message:</p>
<p><pre>{
cmd: "report"
model: "switch"
sid: "158d000128b124"
short_id: 56773
data: {
status: "click",
batteryLevel: 23
}
}</pre></p>
</script>

9
dist/nodes/yeelight/index.js vendored Normal file
View File

@@ -0,0 +1,9 @@
"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 Normal file
View File

@@ -0,0 +1,23 @@
"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 Normal file
View File

@@ -0,0 +1,6 @@
"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"));