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

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