2
0

feat(gateway): move to lumi-aqara

Also add gateway search and subdevices discovery.
Closes #28, closes #27, closes #26, closes #17 and fixes #12
This commit is contained in:
Pierre CLEMENT
2018-01-23 10:48:48 +01:00
parent 13d4282923
commit c1299336cb
35 changed files with 725 additions and 176 deletions

BIN
icons/devices/door-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
icons/devices/mi-all.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
icons/devices/mi-switch.png Normal file

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

BIN
icons/gateway/mijia-io.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
icons/gateway/mijia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

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

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

@@ -10,10 +10,10 @@
"clean": "rimraf dist",
"build": "npm run clean && npm run build:ts && npm run build:ejs && npm run build:icons",
"build:ts": "tsc --allowUnreachableCode -p .",
"build:ejs": "npm run build:ejs:indexes",
"build:ejs": "npm run build:ejs:indexes && npm run build:ejs:devices",
"build:ejs:indexes": "ejs-cli --base-dir src/ --options \"{\\\"NODES_PREFIX\\\": \\\"mi-devices\\\"}\" \"**/index.ejs\" --out dist/",
"build:ejs:devices": "ejs-cli --base-dir src/ --options \"{\\\"NODES_PREFIX\\\": \\\"mi-devices\\\"}\" \"nodes/devices/*.ejs\" --out dist/",
"build:icons": "npm run build:icons:yeelight",
"build:icons": "npm run build:icons:gateway && npm run build:icons:devices && npm run build:icons:actions && npm run build:icons:yeelight",
"build:icons:gateway": "cp -pr icons/gateway dist/nodes/gateway/icons",
"build:icons:devices": "cp -pr icons/devices dist/nodes/devices/icons",
"build:icons:actions": "cp -pr icons/actions dist/nodes/actions/icons",
@@ -28,15 +28,14 @@
],
"node-red": {
"nodes": {
"xiaomi-ht": "node-red-contrib-xiaomi-ht/xiaomi-ht.js",
"xiaomi-magnet": "node-red-contrib-xiaomi-magnet/xiaomi-magnet.js",
"xiaomi-motion": "node-red-contrib-xiaomi-motion/xiaomi-motion.js",
"xiaomi-switch": "node-red-contrib-xiaomi-switch/xiaomi-switch.js",
"xiaomi-ht": "dist/nodes/devices/Sensor.js",
"xiaomi-magnet": "dist/nodes/devices/Magnet.js",
"xiaomi-motion": "dist/nodes/devices/Motion.js",
"xiaomi-switch": "dist/nodes/devices/Switch.js",
"xiaomi-socket": "node-red-contrib-xiaomi-socket/xiaomi-socket.js",
"xiaomi-socket-wifi": "node-red-contrib-xiaomi-socket-wifi/xiaomi-socket-wifi.js",
"xiaomi-all": "node-red-contrib-xiaomi-all/xiaomi-all.js",
"xiaomi-configurator": "node-red-contrib-xiaomi-configurator/xiaomi-configurator.js",
"xiaomi-gateway": "node-red-contrib-xiaomi-gateway/xiaomi-gateway.js",
"xiaomi-all": "dist/nodes/devices/All.js",
"xiaomi-gateway": "dist/nodes/gateway/index.js",
"xiaomi-actions": "node-red-contrib-xiaomi-actions/xiaomi-actions.js",
"xiaomi-yeelight": "dist/nodes/yeelight/index.js"
}

View File

@@ -0,0 +1,47 @@
<script type="text/javascript">
RED.nodes.registerType('<%= NODES_PREFIX %>-gateway', {
category: 'xiaomi',
color: '#3FADB5',
defaults: {
gateway: {value:"", type:"<%= NODES_PREFIX %>-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="<%= NODES_PREFIX %>-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="<%= NODES_PREFIX %>-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"><%= NODES_PREFIX %>-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>

View File

@@ -0,0 +1,62 @@
import { Red, Node, NodeProperties } from "node-red";
import { LumiAqara } from "../../../typings/index";
import { Constants } from "../constants";
export interface IGatewayNode extends Node {
gatewayConf:any;
gateway: LumiAqara.Gateway;
setGateway(gateway:LumiAqara.Gateway);
}
export default (RED:Red) => {
class Gateway {
protected gatewayConf: any;
protected gateway: LumiAqara.Gateway;
constructor(props:NodeProperties){
RED.nodes.createNode(<any> this, props);
this.gatewayConf = RED.nodes.getNode((<any> props).gateway);
this.gateway = null;
(<any> this).status({fill:"red", shape:"ring", text: "offline"});
this.setMessageListener();
}
protected setMessageListener() {
(<any> this).on('input', (msg) => {
if (this.gateway) {
var payload = msg.payload;
// Input from gateway
if(payload.sid && 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;*/
}
(<any> this).send(msg);
}
// Prepare for request
else {
msg.sid = this.gateway.sid;
(<any> this).send(msg);
}
}
});
}
setGateway(gateway:LumiAqara.Gateway) {
this.gateway = gateway;
this.gateway.setPassword(this.gatewayConf.password);
(<any> this).status({fill:"blue", shape:"dot", text: "online"});
this.gateway.on('offline', () => {
this.gateway = null;
(<any> this).status({fill:"red", shape:"ring", text: "offline"});
});
};
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-gateway`, <any> Gateway);
};

View File

@@ -0,0 +1,120 @@
<script type="text/javascript">
RED.nodes.registerType('<%= NODES_PREFIX %>-gateway configurator', {
category: 'config',
defaults: {
name: {value: ""},
ip: {value: ""},
sid: {value: ""},
deviceList: {value:{}}
},
credentials: {
key: { type: "text" }
},
paletteLabel: "gateway configurator",
label: function () {
return this.name || "gateway configurator";
},
oneditprepare: function() {
RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways.forEach(function(gateway, index) {
$('#discovered-gateways').append('<option value="' + gateway.sid + '">' + gateway.sid + ' - ' + gateway.ip + '</option>');
});
var node = this;
function addSubdevice(device) {
var devicesConfig = {
"sensor": {value:"sensor", label:"sensor ht", icon:"icons/node-red-contrib-mi-devices/thermometer-icon.png"},
"magnet": {value:"magnet", label:"magnet", icon:"icons/node-red-contrib-mi-devices/door-icon.png"},
"motion": {value:"motion", label:"motion", icon:"icons/node-red-contrib-mi-devices/motion-icon.png"},
"switch": {value:"switch", label:"switch", icon:"icons/node-red-contrib-mi-devices/mi-switch.png"},
"plug": {value:"plug", label:"plug zigbee", icon:"icons/node-red-contrib-mi-devices/outlet-icon.png"}
};
var row = $('<div/>', {class: "form-row"}).appendTo($('#input-subdevices'));
$('<input/>', {value: device.sid, type: "hidden", name: "sid"}).appendTo(row);
$('<input/>', {value: device.type, type: "hidden", name: "type"}).appendTo(row);
$('<label/>', {for: "node-config-input-name-" + device.sid}).html('<img src="' + devicesConfig[device.type].icon + '" style="width:24px;height:24px;filter:contrast(0);"> ' + device.sid).appendTo(row);
$('<input/>', {id: "node-config-input-name-" + device.sid, type: "text", value: (node.deviceList && node.deviceList[device.sid].name) || devicesConfig[device.type].label}).appendTo(row);
}
$('#discovered-gateways').on('change', function() {
var sid = $('#discovered-gateways').val();
$('#input-subdevices > *').remove();
var gateway = sid && RED.settings.miDevicesGatewayConfiguratorDiscoveredGateways.filter(function(e) { return e.sid == sid })[0];
$("#node-config-input-sid").val(gateway && gateway.sid);
$("#node-config-input-ip").val("");
$("#node-config-input-key").val("");
gateway && gateway.subdevices.forEach(function(device) {
addSubdevice(device);
});
});
$.each(this.deviceList, function(sid, elt) {
addSubdevice({sid: sid, type: elt.type})
});
},
oneditsave: function() {
var node = this;
$('#input-subdevices > *').each(function(i, elt) {
var sid = $(elt).find('input[name=sid]').val();
var type = $(elt).find('input[name=type]').val();
var name = $(elt).find('#node-config-input-name-' + sid).val() || "";
node.deviceList[sid] = {type: type, name: name};
});
}
});
</script>
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-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-ip"><i class="fa fa-compass"></i> IP</label>
<input type="text" id="node-config-input-ip" placeholder="IP">
</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-key"><i class="fa fa-key"></i> Key</label>
<input type="text" id="node-input-key" placeholder="Key">
</div>
<p>Note: use <code>ip</code> or <code>sid</code> - <code>sid</code> is better.</p>
<h4>Devices</h4>
<div id="input-subdevices"></div>
</script>
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-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>

View File

@@ -0,0 +1,44 @@
import { Red, Node, NodeProperties, NodeStatus, ClearNodeStatus } from "node-red";
import { Constants } from "../constants";
import { Searcher } from "./Searcher";
import { LumiAqara } from "../../../typings/index";
export interface IGatewayConfiguratorNode extends Node {
ip:string;
sid:number;
gateway: LumiAqara.Gateway;
on(event: "gatewayFound", listener: () => void): any;
}
export default (RED:Red) => {
class GatewayConfigurator {
ip:string;
sid:number;
_gateway:LumiAqara.Gateway;
constructor(props: NodeProperties) {
RED.nodes.createNode(<any> this, props);
let {ip, sid} = <any> props;
this.sid = sid;
this.ip = ip;
}
set gateway(gateway:LumiAqara.Gateway) {
this._gateway = gateway;
this._gateway.setPassword((<any> this).credentials.key);
(<any> this).emit('gatewayFound');
}
get gateway() {
return this._gateway;
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-gateway configurator`, <any> GatewayConfigurator, {
settings: {
miDevicesGatewayConfiguratorDiscoveredGateways: { value: Searcher.gateways, exportable: true }
},
credentials: { key: {type:"text"} }
});
};

View File

@@ -0,0 +1,55 @@
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-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="<%= NODES_PREFIX %>-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('<%= NODES_PREFIX %>-gateway in',{
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""},
gateway: {value:"", type:"<%= NODES_PREFIX %>-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>

View File

@@ -0,0 +1,70 @@
import {Red, Node, NodeProperties} from 'node-red';
import {LumiAqara} from '../../../typings';
import { Constants } from '../constants';
export interface IGatewayInNode extends Node {
gatewayConf:any;
gateway: LumiAqara.Gateway;
setGateway(gateway:LumiAqara.Gateway);
}
export default (RED:Red) => {
class GatewayIn {
protected gatewayConf: any;
protected gateway: LumiAqara.Gateway;
constructor(props:NodeProperties) {
RED.nodes.createNode(<any> this, props);
this.gatewayConf = RED.nodes.getNode((<any> props).gateway);
(<any>this).status({fill:"red", shape:"ring", text: "offline"});
}
setGateway(gateway:LumiAqara.Gateway) {
this.gateway = gateway;
this.gateway.setPassword(this.gatewayConf.password);
(<any>this).status({fill:"blue", shape:"dot", text: "online"});
this.gateway.on('offline', () => {
this.gateway = null;
(<any>this).status({fill:"red", shape:"ring", text: "offline"});
});
this.gateway.on('subdevice', (device) => {
device.sid = device.getSid();
device.type = device.getType();
device.data = {
voltage: device.getBatteryVoltage(),
batteryLevel: device.getBatteryPercentage()
};
switch (device.type) {
case 'magnet':
device.data.status = device.isOpen() ? 'open' : 'close';
break;
case 'switch':
device.on('click', () => {
// Saaad
});
break;
case 'motion':
break;
case 'sensor':
device.data.temperature = device.getTemperature();
device.data.humidity = device.getHumidity();
device.data.pressure = device.getPressure();
break;
case 'leak':
break;
case 'cube':
break;
};
(<any>this).send({
payload: device
});
});
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-gateway in`, <any> GatewayIn);
};

View File

@@ -0,0 +1,54 @@
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-gateway out">
<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="<%= NODES_PREFIX %>-gateway out">
<p>This node sends <code>msg.payload</code> to the configured Xiaomi Gateway.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('<%= NODES_PREFIX %>-gateway out',{
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value:""},
gateway: {value:"", type:"<%= NODES_PREFIX %>-gateway configurator"}
},
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":"";
},
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,49 @@
import { Red, NodeProperties } from "node-red";
import { LumiAqara } from "../../../typings/index";
import { Constants } from "../constants";
export interface IGatewayOutNode extends Node {
gatewayConf:any;
gateway: LumiAqara.Gateway;
setGateway(gateway:LumiAqara.Gateway);
}
export default (RED:Red) => {
class GatewayOut {
protected gatewayConf: any;
protected gateway: LumiAqara.Gateway;
constructor(props:NodeProperties) {
RED.nodes.createNode(<any> this, props);
this.gatewayConf= RED.nodes.getNode((<any> props).gateway);
(<any> this).status({fill:"red", shape:"ring", text: "offline"});
this.setMessageListener();
}
protected setMessageListener() {
(<any> this).on("input", (msg) => {
if (msg.hasOwnProperty("payload") && this.gateway) {
if(msg.payload.cmd === "write" && !msg.payload.data.key && this.gateway && this.gateway.sid && this.gateway._key) {
msg.payload.data.key = this.gateway._key;
}
this.gateway._sendUnicast(JSON.stringify(msg.payload));
}
});
}
setGateway(gateway) {
this.gateway = gateway;
this.gateway.setPassword(this.gatewayConf.password);
(<any> this).status({fill:"blue", shape:"dot", text: "online"});
this.gateway.on('offline', () => {
this.gateway = null;
(<any> this).status({fill:"red", shape:"ring", text: "offline"});
});
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-gateway out`, <any> GatewayOut);
};

View File

@@ -0,0 +1,42 @@
import { Red } from "node-red";
import * as LumiAqara from 'lumi-aqara';
import { Constants } from "../constants";
import { IGatewayConfiguratorNode } from "./GatewayConfigurator";
export class Searcher {
static _gateways:LumiAqara.Gateway[] = [];
static discover(RED:Red) {
new Promise(() => {
const aqara = new LumiAqara();
aqara.on('gateway', (gateway:LumiAqara.Gateway) => {
let frontGateway = {
sid: gateway.sid,
ip: gateway.ip,
subdevices: []
};
this._gateways.push(frontGateway);
gateway.on('subdevice', (device:LumiAqara.SubDevice) => {
frontGateway.subdevices.push({
sid: device.getSid(),
type: device.getType()
});
});
RED.nodes.eachNode((tmpNode) => {
if(tmpNode.type.indexOf(`${Constants.NODES_PREFIX}-gateway configurator`) === 0) {
let tmpNodeInst = <IGatewayConfiguratorNode> RED.nodes.getNode(tmpNode.id);
if(tmpNodeInst && (tmpNodeInst.ip === gateway.ip || tmpNodeInst.sid === gateway.sid)) {
tmpNodeInst.gateway = gateway;
}
}
});
});
});
}
static get gateways():LumiAqara.Gateway {
return this._gateways;
}
}

View File

@@ -0,0 +1,4 @@
<%- include('./GatewayConfigurator', {}); %>
<%- include('./Gateway', {}); %>
<%- include('./GatewayIn', {}); %>
<%- include('./GatewayOut', {}); %>

View File

@@ -0,0 +1,17 @@
import { Red, NodeProperties } from "node-red";
import * as LumiAqara from 'lumi-aqara';
import { Searcher } from "./Searcher";
import {default as GatewayConfigurator} from "./GatewayConfigurator";
import {default as Gateway} from "./Gateway";
import {default as GatewayIn} from "./GatewayIn";
import {default as GatewayOut} from "./GatewayOut";
export = (RED:Red) => {
Searcher.discover(RED);
GatewayConfigurator(RED);
Gateway(RED);
GatewayIn(RED);
GatewayOut(RED);
};

View File

@@ -2,6 +2,7 @@ import { Red } from "node-red";
import * as YeelightSearch from 'yeelight-wifi';
import { Constants } from "../constants";
import { IYeelightConfiguratorNode } from "./YeelightConfigurator";
export class Searcher {
static _bulbs:any[] = [];
@@ -17,9 +18,9 @@ export class Searcher {
});
RED.nodes.eachNode((tmpNode) => {
if(tmpNode.type.indexOf(`${Constants.NODES_PREFIX}-yeelight configurator`) === 0) {
let tmpNodeInst = <any> RED.nodes.getNode(tmpNode.id);
if(tmpNodeInst.ip === bulb.hostname || tmpNodeInst.sid === parseInt(bulb.id)) {
tmpNodeInst.setBulb(bulb);
let tmpNodeInst = <IYeelightConfiguratorNode> RED.nodes.getNode(tmpNode.id);
if(tmpNodeInst.ip == bulb.hostname || tmpNodeInst.sid == parseInt(bulb.id)) {
tmpNodeInst.bulb = bulb;
}
}
});

View File

@@ -34,7 +34,7 @@
</div>
<hr>
<div class="form-row">
<label for="node-config-input-name"><i class="icon-tag"></i> Name</label>
<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">

View File

@@ -1,8 +1,8 @@
import { Red, NodeProperties, NodeStatus, ClearNodeStatus } from "node-red";
import { Red, Node, NodeProperties, NodeStatus, ClearNodeStatus } from "node-red";
import { Constants } from "../constants";
import { Searcher } from "./Searcher";
export interface IYeelightConfiguratorNode {
export interface IYeelightConfiguratorNode extends Node {
ip:string;
sid:number;
bulb:any;

View File

@@ -1,10 +1,10 @@
<script type="text/javascript">
RED.nodes.registerType('<%= NODE_PREFIX %>-yeelight out', {
RED.nodes.registerType('<%= NODES_PREFIX %>-yeelight out', {
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value: ""},
yeelight: {value:"", type:"<%= NODE_PREFIX %>-yeelight configurator"}
yeelight: {value:"", type:"<%= NODES_PREFIX %>-yeelight configurator"}
},
inputs: 1,
outputs: 0,
@@ -35,18 +35,18 @@
});
</script>
<script type="text/x-red" data-template-name="<%= NODE_PREFIX %>-yeelight out">
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-yeelight out">
<div class="form-row">
<label for="node-input-yeelight"><i class="icon-tag"></i> Yeelight</label>
<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="icon-tag"></i> Name</label>
<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="<%= NODE_PREFIX %>-yeelight out">
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-yeelight out">
<p>The Xiaomi Yeelight node</p>
<h3>Inputs</h3>

View File

@@ -3,19 +3,19 @@ import { Constants } from "../constants";
import { IYeelightConfiguratorNode } from "./YeelightConfigurator";
export interface IYeelightOutNode {
yeelightNode:IYeelightConfiguratorNode;
yeelightConfNode:IYeelightConfiguratorNode;
}
export default (RED:Red) => {
class YeelightOut implements IYeelightOutNode {
yeelightNode:IYeelightConfiguratorNode;
yeelightConfNode:IYeelightConfiguratorNode;
constructor(props: NodeProperties) {
RED.nodes.createNode(<any> this, props);
this.yeelightNode = <any> RED.nodes.getNode((<any> props).yeelight);
this.yeelightConfNode = <any> RED.nodes.getNode((<any> props).yeelight);
(<any> this).status({fill: "red", shape: "ring", text: "offline"});
this.yeelightNode && this.yeelightNode.on('bulbFound', () => {
this.yeelightConfNode && this.yeelightConfNode.on('bulbFound', () => {
(<any>this).status({fill:"blue", shape:"dot", text: "online"});
});
@@ -24,23 +24,23 @@ export default (RED:Red) => {
protected setListener() {
(<any> this).on('input', (msg) => {
if (this.yeelightNode.bulb) {
if (this.yeelightConfNode.bulb) {
if(msg.payload === "on") {
this.yeelightNode.bulb.turnOn();
this.yeelightConfNode.bulb.turnOn();
}
else if(msg.payload === "off") {
this.yeelightNode.bulb.turnOff();
this.yeelightConfNode.bulb.turnOff();
}
else if(msg.payload === "toggle") {
this.yeelightNode.bulb.toggle();
this.yeelightConfNode.bulb.toggle();
}
if(msg.payload.color !== undefined) {
// TODO: revoir la couleur
this.yeelightNode.bulb.setRGB(msg.payload.color);
this.yeelightConfNode.bulb.setRGB(msg.payload.color);
}
if(msg.payload.brightness !== undefined) {
this.yeelightNode.bulb.setBrightness(msg.payload.brightness);
this.yeelightConfNode.bulb.setBrightness(msg.payload.brightness);
}
}
});

1
typings/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from './lumi-aqara.d';

129
typings/lumi-aqara.d.ts vendored Normal file
View File

@@ -0,0 +1,129 @@
import { EventEmitter } from "events";
export module LumiAqara {
interface Color {
r:number;
g:number;
b:number;
}
export interface Gateway extends EventEmitter {
ip: string;
sid: string;
ready: boolean;
password: string;
color: Color;
intensity: number;
/* Should be public */
_key: string;
setPassword(password:string);
setColor(color:Color);
setIntensity(intensity:number);
/* Should be public */
_sendUnicast(message:string);
}
/*************** Subdevices ***************/
interface SubDeviceOptions {
sid:string;
type?:string;
}
export interface SubDevice extends EventEmitter {
constructor(opts:SubDeviceOptions);
getSid(): string;
getType(): string;
getBatteryVoltage(): number;
getBatteryPercentage(): number;
}
export interface Cube extends SubDevice {
constructor(opts:SubDeviceOptions);
getType(): "cube";
getStatus(): "rotate"|string;
getRotateDegrees(): number|null;
/**
* @event Emitted when a device value updated.
*/
on(event: "update", listener: () => void): any;
}
export interface Leak extends SubDevice {
constructor(opts:SubDeviceOptions);
getType(): "leak";
isLeaking(): boolean;
/**
* @event Emitted when a device value updated.
*/
on(event: "update", listener: () => void): any;
}
export interface Magnet extends SubDevice {
constructor(opts:SubDeviceOptions);
getType(): "magnet";
isOpen(): boolean;
/**
* @event Emitted when a the door/window just opend.
*/
on(event: "open", listener: () => void): any;
/**
* @event Emitted when a the door/window just closed.
*/
on(event: "close", listener: () => void): any;
}
export interface Motion extends SubDevice {
constructor(opts:SubDeviceOptions);
getType(): "motion";
hasMotion(): boolean;
getLux(): number;
getSecondsSinceMotion(): number;
/**
* @event Emitted when a motion has been detected.
*/
on(event: "motion", listener: () => void): any;
/**
* @event Emitted after a motion ends.
*/
on(event: "noMotion", listener: () => void): any;
}
export interface Sensor extends SubDevice {
constructor(opts:SubDeviceOptions);
getType(): "sensor";
/**
* @returns Temperature in degrees.
*/
getTemperature(): number;
/**
* @returns Humidity in percent.
*/
getHumidity(): number;
/**
* @returns Pressure in kPa
*/
getPressure(): number;
/**
* @event Emitted when a device value updated.
*/
on(event: "update", listener: () => void): any;
}
export interface Switch extends SubDevice {
constructor(opts:SubDeviceOptions);
getType(): "switch";
/**
* @event Emitted when a click is done on the switch.
*/
on(event: "click", listener: () => void): any;
/**
* @event Emitted when a double click is done on the switch.
*/
on(event: "doubleClick", listener: () => void): any;
/**
* @event Emitted when a double click is done on the switch.
*/
on(event: "doubleClick", listener: () => void): any;
/**
* @event Emitted when a long press is done.
*/
on(event: "longClickPress", listener: () => void): any;
/**
* @event Emitted when release the switch after a long press.
*/
on(event: "longClickRelease", listener: () => void): any;
}
}