feat(yeelight): move to yeelight-wifi

Also add the configurator node, typescript & ejs.
Close #29 #25 #24
This commit is contained in:
Pierre CLEMENT
2018-01-20 13:23:08 +01:00
parent 72b7bed6ee
commit 13d4282923
12 changed files with 281 additions and 73 deletions

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,57 +0,0 @@
const miDevicesUtils = require('../src/utils');
const Yeelight = require("yeelight2");
module.exports = (RED) => {
function XiaomiYeelightOutputNode(config) {
RED.nodes.createNode(this, config);
this.ip = config.ip;
this.port = config.port;
this.status({fill:"grey", shape:"ring", text:"na"});
this.setupConnection = function(){
try {
this.light = Yeelight(`yeelight://${this.ip}:${this.port}`);
this.status({fill:"blue", shape:"dot", text:"connected"});
} catch(err) {
this.status({fill:"red",shape:"ring",text:err.message});
this.light = null;
this.error(err);
// try to reconnect in 5 minutes
window.setTimeout((function(self) {
return function() {
self.setupConnection.apply(self, arguments);
}
})(this), 1000*60*5);
}
}
if (this.ip && this.port) {
this.setupConnection();
this.on('input', (msg) => {
if(msg.payload === "on") {
this.light && this.light.set_power('on');
}
else if(msg.payload === "off") {
this.light && this.light.set_power('off');
}
else if(msg.payload === "toggle") {
this.light && this.light.toggle();
}
if(msg.payload.color !== undefined) {
this.light && this.light.set_rgb(msg.payload.color);
}
if(msg.payload.brightness !== undefined) {
this.light && this.light.set_bright(msg.payload.brightness);
}
});
this.on('close', () => {
this.light && this.light.exit();
});
}
}
RED.nodes.registerType("xiaomi-yeelight out", XiaomiYeelightOutputNode);
};

View File

@@ -6,6 +6,19 @@
"type": "git",
"url": "git+ssh://git@github.com:pierrecle/node-red-contrib-mi-devices.git"
},
"scripts": {
"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: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: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",
"build:icons:yeelight": "cp -pr icons/yeelight dist/nodes/yeelight/icons"
},
"license": "MIT",
"keywords": [
"Xiaomi",
@@ -25,7 +38,7 @@
"xiaomi-configurator": "node-red-contrib-xiaomi-configurator/xiaomi-configurator.js",
"xiaomi-gateway": "node-red-contrib-xiaomi-gateway/xiaomi-gateway.js",
"xiaomi-actions": "node-red-contrib-xiaomi-actions/xiaomi-actions.js",
"xiaomi-yeelight": "node-red-contrib-xiaomi-yeelight/xiaomi-yeelight.js"
"xiaomi-yeelight": "dist/nodes/yeelight/index.js"
}
},
"author": "Pierre CLEMENT",
@@ -34,10 +47,18 @@
},
"dependencies": {
"cryptojs": "^2.5.3",
"miio": "0.13.0",
"yeelight2": "^1.3.5"
"lumi-aqara": "^1.4.0",
"miio": "^0.14.1",
"yeelight-wifi": "^2.3.0"
},
"engines": {
"node": ">=4.4.5"
},
"devDependencies": {
"@types/node-red": "^0.17.1",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"rimraf": "^2.6.2",
"typescript": "^2.6.2"
}
}

3
src/nodes/constants.ts Normal file
View File

@@ -0,0 +1,3 @@
export class Constants {
static readonly NODES_PREFIX = "mi-devices";
}

View File

@@ -0,0 +1,33 @@
import { Red } from "node-red";
import * as YeelightSearch from 'yeelight-wifi';
import { Constants } from "../constants";
export class Searcher {
static _bulbs:any[] = [];
static discover(RED:Red) {
new Promise(() => {
(new YeelightSearch()).on('found', (bulb:any) => {
this._bulbs.push({
name: bulb.name,
model: bulb.model,
sid: parseInt(bulb.id),
ip: bulb.hostname
});
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);
}
}
});
});
});
}
static get bulbs() {
return this._bulbs;
}
}

View File

@@ -0,0 +1,73 @@
<script type="text/javascript">
RED.nodes.registerType('<%= NODES_PREFIX %>-yeelight configurator', {
category: 'config',
defaults: {
name: {value: ""},
ip: {value: ""},
sid: {value: ""}
},
label: function () {
return this.name || "yeelight configurator";
},
oneditprepare: function() {
RED.settings.miDevicesYeelightConfiguratorDiscoveredBulbs.forEach(function(bulb, index) {
$('#discovered-bulbs').append('<option value="' + bulb.sid + '">' + (bulb.name || bulb.sid) + ' - ' + bulb.model + ' - ' + bulb.ip + '</option>');
});
var node = this;
$('#discovered-bulbs').on('change', function() {
var sid = $('#discovered-bulbs').val();
var bulb = sid && RED.settings.miDevicesYeelightConfiguratorDiscoveredBulbs.filter(function(e) { return e.sid == sid })[0];
$("#node-config-input-name").val(bulb && bulb.name);
$("#node-config-input-sid").val(bulb && bulb.sid);
$("#node-config-input-ip").val("");
});
}
});
</script>
<script type="text/x-red" data-template-name="<%= NODES_PREFIX %>-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="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="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>
<p>Note: use <code>ip</code> or <code>sid</code> - <code>sid</code> is better.</p>
</script>
<script type="text/x-red" data-help-name="<%= NODES_PREFIX %>-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>

View File

@@ -0,0 +1,41 @@
import { Red, NodeProperties, NodeStatus, ClearNodeStatus } from "node-red";
import { Constants } from "../constants";
import { Searcher } from "./Searcher";
export interface IYeelightConfiguratorNode {
ip:string;
sid:number;
bulb:any;
on(event: "bulbFound", listener: () => void): any;
}
export default (RED:Red) => {
class YeelightConfigurator {
ip:string;
sid:number;
_bulb:any;
constructor(props: NodeProperties) {
RED.nodes.createNode(<any> this, props);
let {ip, sid} = <any> props;
this.sid = sid;
this.ip = ip;
}
set bulb(bulb) {
this._bulb = bulb;
(<any> this).emit('bulbFound');
}
get bulb() {
return this._bulb;
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-yeelight configurator`, <any> YeelightConfigurator, {
settings: {
miDevicesYeelightConfiguratorDiscoveredBulbs: { value: Searcher.bulbs, exportable: true }
}
});
};

View File

@@ -1,11 +1,10 @@
<script type="text/javascript">
RED.nodes.registerType('xiaomi-yeelight out', {
RED.nodes.registerType('<%= NODE_PREFIX %>-yeelight out', {
category: 'xiaomi in out',
color: '#087F8A',
defaults: {
name: {value: ""},
ip: {value: ""},
port: {value:55443, required: true}
yeelight: {value:"", type:"<%= NODE_PREFIX %>-yeelight configurator"}
},
inputs: 1,
outputs: 0,
@@ -14,26 +13,40 @@
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="xiaomi-yeelight out">
<script type="text/x-red" data-template-name="<%= NODE_PREFIX %>-yeelight out">
<div class="form-row">
<label for="node-input-yeelight"><i class="icon-tag"></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>
<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">
</div>
<div class="form-row">
<label for="node-input-port"><i class="icon-tag"></i> Port</label>
<input type="text" id="node-input-port" placeholder="Port">
</div>
</script>
<script type="text/x-red" data-help-name="xiaomi-yeelight out">
<script type="text/x-red" data-help-name="<%= NODE_PREFIX %>-yeelight out">
<p>The Xiaomi Yeelight node</p>
<h3>Inputs</h3>

View File

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

View File

@@ -0,0 +1,2 @@
<%- include('./YeelightConfigurator', {}); %>
<%- include('./YeelightOut', {}); %>

View File

@@ -0,0 +1,13 @@
import { Red, NodeProperties } from "node-red";
import * as YeelightSearch from 'yeelight-wifi';
import { Searcher } from "./Searcher";
import {default as YeelightConfigurator} from "./YeelightConfigurator";
import {default as YeelightOut} from "./YeelightOut";
export = (RED:Red) => {
Searcher.discover(RED);
YeelightConfigurator(RED);
YeelightOut(RED);
};

14
tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"inlineSourceMap": false,
"outDir": "dist/",
"rootDir": "./src",
"moduleResolution": "node"
},
"include": [
"src/**/*.ts",
"typings"
]
}