feat(yeelight): handle yeelights

This commit is contained in:
Pierre CLEMENT
2018-03-21 22:52:10 +01:00
parent 50df65d7fc
commit c5afb43b47
31 changed files with 219 additions and 310 deletions

View File

@@ -1,104 +0,0 @@
<!-- The "on" Node -->
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions on',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "on",
icon: "mi-on.png",
label: function() {
return this.name||"power on";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-actions on">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-actions on">
<p>
Turn input device to on.
</p>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway/yeelight out node.</li>
</ol>
</script>
<!-- The "off" Node -->
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions off',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "off",
icon: "mi-off.png",
label: function() {
return this.name||"power off";
}
});
</script>
<script type="text/x-red" data-template-name="mi-devices-actions off">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="mi-devices-actions off">
<p>
Turn input device to off.
</p>
<h3>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway/yeelight out node.</li>
</ol>
</script>
<!-- The "toggle" Node -->
<script type="text/javascript">
RED.nodes.registerType('mi-devices-actions toggle',{
category: 'xiaomi actions',
color: '#64C4CD',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
paletteLabel: "toggle",
icon: "mi-toggle.png",
label: function() {
return this.name||"toggle power";
}
});
</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>Outputs</h3>
<ol class="node-ports">
<li>Message to connect to a gateway/yeelight out node.</li>
</ol>
</script>

View File

@@ -1,57 +0,0 @@
const miDevicesUtils = require('../src/utils');
module.exports = (RED) => {
/*********************************************
Turn device on
*********************************************/
function XiaomiActionPowerOn(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
if(msg.sid){
msg.payload = {
cmd: "write",
data: { status: "on", sid: msg.sid }
};
}
else {
msg.payload = "on";
}
this.send(msg);
});
}
RED.nodes.registerType("mi-devices-actions on", XiaomiActionPowerOn);
/*********************************************
Turn device off
*********************************************/
function XiaomiActionPowerOff(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
if(msg.sid){
msg.payload = {
cmd: "write",
data: { status: "off", sid: msg.sid }
};
}
else {
msg.payload = "off";
}
this.send(msg);
});
}
RED.nodes.registerType("mi-devices-actions off", XiaomiActionPowerOff);
/*********************************************
Toggle device
*********************************************/
function XiaomiActionToggle(config) {
RED.nodes.createNode(this, config);
this.on('input', (msg) => {
msg.payload = "toggle";
this.send(msg);
});
}
RED.nodes.registerType("mi-devices-actions toggle", XiaomiActionToggle);
}

View File

@@ -46,8 +46,7 @@
},
"dependencies": {
"cryptojs": "^2.5.3",
"lumi-aqara": "^1.4.0",
"miio": "^0.15.2",
"miio": "^0.15.4",
"yeelight-wifi": "^2.3.0"
},
"engines": {

View File

@@ -4,7 +4,7 @@ import * as crypto from 'crypto';
import {GatewayServer} from "./GatewayServer";
import {GatewayMessage, GatewaySubdevice, Magnet, Motion, Switch, Weather} from "./";
import * as MessageData from "./GatewayMessageData";
import {Color} from "../utils/Color";
import {Color} from "../../utils/Color";
export class Gateway extends events.EventEmitter {
static iv: Buffer = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);

View File

@@ -1,6 +1,6 @@
import * as events from 'events';
import * as dgram from "dgram";
import {Gateway} from "./Gateway";
import {Gateway} from "./";
import Timer = NodeJS.Timer;
import {GatewayMessage} from "./GatewayMessage";
@@ -92,36 +92,6 @@ export class GatewayServer extends events.EventEmitter {
}
gatewaySid && this._gateways[gatewaySid] && this._gateways[gatewaySid].handleMessage(msg);
/*if(remote.address == this.addr) {
var msg = message.toString('utf8');
var jsonMsg = JSON.parse(msg);
if(jsonMsg.data) {
jsonMsg.data = JSON.parse(jsonMsg.data) || jsonMsg.data;
if(jsonMsg.data.voltage) {
jsonMsg.data.batteryLevel = miDevicesUtils.computeBatteryLevel(jsonMsg.data.voltage);
}
}
msg = { payload: jsonMsg };
if(this.gateway && jsonMsg.data.ip && jsonMsg.data.ip === this.gateway.ip) {
if(jsonMsg.token) {
this.gateway.lastToken = jsonMsg.token;
if(!this.gateway.sid) {
this.gateway.sid = jsonMsg.sid;
}
}
RED.nodes.eachNode((tmpNode) => {
if(tmpNode.type.indexOf("xiaomi-gateway") === 0 && tmpNode.gateway == this.gatewayNodeId) {
let tmpNodeInst = RED.nodes.getNode(tmpNode.id);
if(tmpNode.type === "xiaomi-gateway out" && !this.gateway.lastToken) {
tmpNodeInst.status({fill:"yellow", shape:"ring", text: "waiting input"});
}
tmpNodeInst.status({fill:"blue", shape:"dot", text: "online"});
}
});
}
this.send(msg);
}*/
});
return new Promise((resolve, reject) => {

View File

@@ -1,3 +1,4 @@
export * from './';
export * from './Gateway';
export * from './GatewayMessage';
export * from './GatewayServer';

View File

@@ -0,0 +1,47 @@
import * as events from 'events';
import * as YeelightSearch from 'yeelight-wifi';
export class YeelightServer extends events.EventEmitter {
private static instance: YeelightServer;
private _bulbs: { [sid: string]: any } = {};
private _bulbsJson: { [sid: string]: any } = {};
static getInstance() {
if (!this.instance) {
this.instance = new YeelightServer();
}
return this.instance;
}
get bulbs(): { [sid: string]: any } {
return this._bulbsJson;
}
getBulb(sid) {
return this._bulbs[sid];
}
discover() {
new Promise(() => {
(new YeelightSearch()).on('found', (bulb: any) => {
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
};
}
}

View File

View File

@@ -15,13 +15,11 @@ export default (RED: Red) => {
protected setListeners() {
(<any> this).on('input', (msg) => {
if (msg.sid) {
msg.payload = {
action: "setLight",
color: msg.color || this.color,
brightness: msg.brightness || this.brightness
};
}
msg.payload = {
action: "setLight",
color: msg.color || this.color,
brightness: msg.brightness || this.brightness
};
(<any> this).send(msg);
});
}

View File

@@ -0,0 +1,20 @@
import {Red, NodeProperties} from "node-red";
import {Constants} from "../constants";
export default (RED: Red, action: string) => {
class ToggleAction {
constructor(props: NodeProperties) {
RED.nodes.createNode(<any> this, props);
(<any> this).setListeners();
}
protected setListeners() {
(<any> this).on('input', (msg) => {
msg.payload = { action };
(<any> this).send(msg);
});
}
}
RED.nodes.registerType(`${Constants.NODES_PREFIX}-actions ${action}`, <any> ToggleAction);
};

View File

@@ -32,4 +32,28 @@
<%- include('./Light', {}) %>
<%- include('./GatewayPlaySound', {}) %>
<%- include('./GatewayStopSound', {}) %>
<%- include('./GatewayStopSound', {}) %>
<%# ---------------------------------- turn_on ---------------------------------- %>
<%- include('./Action', {
type: "turn_on",
label: "turn on",
icon: "mi-on",
docTitle: "Turn device on."
}) %>
<%# ---------------------------------- double_click ---------------------------------- %>
<%- include('./Action', {
type: "turn_off",
label: "turn off",
icon: "mi-off",
docTitle: "Turn device off."
}) %>
<%# ---------------------------------- double_click ---------------------------------- %>
<%- include('./Action', {
type: "toggle",
label: "toggle",
icon: "mi-toggle",
docTitle: "Toggle device."
}) %>

View File

@@ -5,6 +5,7 @@ import {default as WriteAction} from './WriteAction';
import {default as Light} from './Light';
import {default as GatewayPlaySound} from './GatewayPlaySound';
import {default as GatewayStopSound} from './GatewayStopSound';
import {default as ToggleAction} from './ToggleAction';
export = (RED: Red) => {
["read", "get_id_list"].forEach((action) => {
@@ -17,4 +18,7 @@ export = (RED: Red) => {
Light(RED);
GatewayPlaySound(RED);
GatewayStopSound(RED);
["turn_on", "turn_off", "toggle"].forEach(action => {
ToggleAction(RED, action);
});
};

View File

@@ -1,5 +1,4 @@
import { Red, NodeProperties } from "node-red";
import * as LumiAqara from 'lumi-aqara';
import { Red } from "node-red";
import {default as All} from "./All";
import {default as Plug} from "./Plug";

View File

@@ -1,5 +1,4 @@
import { Red, Node, NodeProperties } from "node-red";
import { LumiAqara } from "../../../typings/index";
import { Constants } from "../constants";
export interface IGatewayNode extends Node {

View File

@@ -1,8 +1,8 @@
import {Red, Node, NodeProperties} from "node-red";
import {Constants} from "../constants";
import {GatewayServer} from "../../devices/GatewayServer";
import {Gateway} from "../../devices/Gateway";
import {GatewaySubdevice} from "../../devices/GatewaySubdevice";
import {GatewayServer} from "../../devices/gateway/GatewayServer";
import {Gateway} from "../../devices/gateway/Gateway";
import {GatewaySubdevice} from "../../devices/gateway/GatewaySubdevice";
import {isString} from "util";
export interface IGatewayConfiguratorNode extends Node {

View File

@@ -1,6 +1,6 @@
import {Red, Node, NodeProperties} from 'node-red';
import {Constants} from '../constants';
import {Gateway} from "../../devices/Gateway";
import {Gateway} from "../../devices/gateway/Gateway";
export interface IGatewayInNode extends Node {
gatewayConf: any;

View File

@@ -1,6 +1,6 @@
import {Red, NodeProperties} from "node-red";
import {Constants} from "../constants";
import {GatewayServer} from "../../devices/GatewayServer";
import {GatewayServer} from "../../devices/gateway/GatewayServer";
export default (RED: Red) => {
class GatewayOut {

View File

@@ -1,7 +1,6 @@
import { Red, NodeProperties } from "node-red";
import * as LumiAqara from 'lumi-aqara';
import { Red } from "node-red";
import { GatewayServer } from "../../devices/GatewayServer";
import { GatewayServer } from "../../devices/gateway/GatewayServer";
import {default as GatewayConfigurator} from "./GatewayConfigurator";
import {default as Gateway} from "./Gateway";
import {default as GatewayIn} from "./GatewayIn";

View File

@@ -1,34 +0,0 @@
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[] = [];
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 = <IYeelightConfiguratorNode> RED.nodes.getNode(tmpNode.id);
if(tmpNodeInst.ip == bulb.hostname || tmpNodeInst.sid == parseInt(bulb.id)) {
tmpNodeInst.bulb = bulb;
}
}
});
});
});
}
static get bulbs() {
return this._bulbs;
}
}

View File

@@ -3,23 +3,22 @@
category: 'config',
defaults: {
name: {value: ""},
ip: {value: ""},
sid: {value: ""}
},
label: function () {
return this.name || "yeelight conf";
},
oneditprepare: function() {
RED.settings.miDevicesYeelightConfiguratorDiscoveredBulbs.forEach(function(bulb, index) {
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>');
});
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];
var bulb = foundBulbs[sid];
$("#node-config-input-name").val(bulb && bulb.name);
$("#node-config-input-sid").val(bulb && bulb.sid);
$("#node-config-input-ip").val("");
});
}
});
@@ -37,15 +36,10 @@
<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>
<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">

View File

@@ -1,31 +1,49 @@
import { Red, Node, NodeProperties, NodeStatus, ClearNodeStatus } from "node-red";
import { Constants } from "../constants";
import { Searcher } from "./Searcher";
import {Red, Node, NodeProperties, NodeStatus, ClearNodeStatus} from "node-red";
import {Constants} from "../constants";
import {YeelightServer} from "../../devices/yeelight/YeelightServer";
export interface IYeelightConfiguratorNode extends Node {
ip:string;
sid:number;
bulb:any;
ip: string;
sid: number;
bulb: any;
on(event: "bulbFound", listener: () => void): any;
on(event: "bulb-online", listener: () => void): any;
on(event: "bulb-offline", listener: () => void): any;
}
export default (RED:Red) => {
export default (RED: Red) => {
class YeelightConfigurator {
ip:string;
sid:number;
_bulb:any;
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;
let {sid} = <any> props;
this.sid = parseInt(sid);
if (this.sid) {
this.setBulb();
}
let server = YeelightServer.getInstance();
server.on('yeelight-online', (sid) => {
if (sid === this.sid) {
this.setBulb();
(<any> this).emit('bulb-online');
}
});
server.on('yeelight-offline', (sid) => {
if (sid === this.sid) {
this._bulb = null;
(<any> this).emit('bulb-offline');
}
});
}
set bulb(bulb) {
this._bulb = bulb;
(<any> this).emit('bulbFound');
protected setBulb() {
this._bulb = YeelightServer.getInstance().getBulb(this.sid);
}
get bulb() {
@@ -35,7 +53,10 @@ export default (RED:Red) => {
RED.nodes.registerType(`${Constants.NODES_PREFIX}-yeelight configurator`, <any> YeelightConfigurator, {
settings: {
miDevicesYeelightConfiguratorDiscoveredBulbs: { value: Searcher.bulbs, exportable: true }
miDevicesYeelightConfiguratorDiscoveredBulbs: {
value: YeelightServer.getInstance().bulbs,
exportable: true
}
}
});
};

View File

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

View File

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

View File

@@ -5,7 +5,8 @@
"inlineSourceMap": false,
"outDir": "dist/",
"rootDir": "./src",
"moduleResolution": "node"
"moduleResolution": "node",
"noImplicitAny": false
},
"include": [
"src/**/*.ts",