diff --git a/README.md b/README.md index b551c84..ee78323 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@ The following devices are currently supported: * Aqara smart wireless switch * Motion sensor * Aqara Motion sensor -* Power plug (zigbee) -* Power plug (wifi) * Yeelight White (mono) * Yeelight RGB (color) diff --git a/dist/devices/gateway/Gateway.js b/dist/devices/gateway/Gateway.js new file mode 100644 index 0000000..9ce7046 --- /dev/null +++ b/dist/devices/gateway/Gateway.js @@ -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; diff --git a/dist/devices/gateway/GatewayMessage.js b/dist/devices/gateway/GatewayMessage.js new file mode 100644 index 0000000..b90d398 --- /dev/null +++ b/dist/devices/gateway/GatewayMessage.js @@ -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; diff --git a/dist/devices/gateway/GatewayMessageData.js b/dist/devices/gateway/GatewayMessageData.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/dist/devices/gateway/GatewayMessageData.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/devices/gateway/GatewayServer.js b/dist/devices/gateway/GatewayServer.js new file mode 100644 index 0000000..6cc4a3f --- /dev/null +++ b/dist/devices/gateway/GatewayServer.js @@ -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; diff --git a/dist/devices/gateway/GatewaySubdevice.js b/dist/devices/gateway/GatewaySubdevice.js new file mode 100644 index 0000000..cb4aa50 --- /dev/null +++ b/dist/devices/gateway/GatewaySubdevice.js @@ -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; diff --git a/dist/devices/gateway/Magnet.js b/dist/devices/gateway/Magnet.js new file mode 100644 index 0000000..6641a7e --- /dev/null +++ b/dist/devices/gateway/Magnet.js @@ -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; diff --git a/dist/devices/gateway/Motion.js b/dist/devices/gateway/Motion.js new file mode 100644 index 0000000..49d9cac --- /dev/null +++ b/dist/devices/gateway/Motion.js @@ -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; diff --git a/dist/devices/gateway/Switch.js b/dist/devices/gateway/Switch.js new file mode 100644 index 0000000..06bc156 --- /dev/null +++ b/dist/devices/gateway/Switch.js @@ -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; diff --git a/dist/devices/gateway/Weather.js b/dist/devices/gateway/Weather.js new file mode 100644 index 0000000..c836a20 --- /dev/null +++ b/dist/devices/gateway/Weather.js @@ -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; diff --git a/dist/devices/gateway/index.js b/dist/devices/gateway/index.js new file mode 100644 index 0000000..969aef2 --- /dev/null +++ b/dist/devices/gateway/index.js @@ -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")); diff --git a/dist/devices/yeelight/YeelightServer.js b/dist/devices/yeelight/YeelightServer.js new file mode 100644 index 0000000..569bea9 --- /dev/null +++ b/dist/devices/yeelight/YeelightServer.js @@ -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; diff --git a/dist/devices/yeelight/index.js b/dist/devices/yeelight/index.js new file mode 100644 index 0000000..e69de29 diff --git a/dist/nodes/actions/GatewayPlaySound.js b/dist/nodes/actions/GatewayPlaySound.js new file mode 100644 index 0000000..5756947 --- /dev/null +++ b/dist/nodes/actions/GatewayPlaySound.js @@ -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); +}; diff --git a/dist/nodes/actions/GatewayStopSound.js b/dist/nodes/actions/GatewayStopSound.js new file mode 100644 index 0000000..08431f4 --- /dev/null +++ b/dist/nodes/actions/GatewayStopSound.js @@ -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); +}; diff --git a/dist/nodes/actions/Light.js b/dist/nodes/actions/Light.js new file mode 100644 index 0000000..544ac4c --- /dev/null +++ b/dist/nodes/actions/Light.js @@ -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); +}; diff --git a/dist/nodes/actions/ReadAction.js b/dist/nodes/actions/ReadAction.js new file mode 100644 index 0000000..8bcf4cb --- /dev/null +++ b/dist/nodes/actions/ReadAction.js @@ -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); +}; diff --git a/dist/nodes/actions/ToggleAction.js b/dist/nodes/actions/ToggleAction.js new file mode 100644 index 0000000..cdcd479 --- /dev/null +++ b/dist/nodes/actions/ToggleAction.js @@ -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); +}; diff --git a/dist/nodes/actions/WriteAction.js b/dist/nodes/actions/WriteAction.js new file mode 100644 index 0000000..839f1fa --- /dev/null +++ b/dist/nodes/actions/WriteAction.js @@ -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); +}; diff --git a/dist/nodes/actions/icons/mi-bulb.png b/dist/nodes/actions/icons/mi-bulb.png new file mode 100644 index 0000000..5eae146 Binary files /dev/null and b/dist/nodes/actions/icons/mi-bulb.png differ diff --git a/dist/nodes/actions/icons/mi-click.png b/dist/nodes/actions/icons/mi-click.png new file mode 100644 index 0000000..f04c2cf Binary files /dev/null and b/dist/nodes/actions/icons/mi-click.png differ diff --git a/dist/nodes/actions/icons/mi-double-click.png b/dist/nodes/actions/icons/mi-double-click.png new file mode 100644 index 0000000..23313ed Binary files /dev/null and b/dist/nodes/actions/icons/mi-double-click.png differ diff --git a/dist/nodes/actions/icons/mi-list.png b/dist/nodes/actions/icons/mi-list.png new file mode 100644 index 0000000..af9bb2f Binary files /dev/null and b/dist/nodes/actions/icons/mi-list.png differ diff --git a/dist/nodes/actions/icons/mi-mute.png b/dist/nodes/actions/icons/mi-mute.png new file mode 100644 index 0000000..0a97e87 Binary files /dev/null and b/dist/nodes/actions/icons/mi-mute.png differ diff --git a/dist/nodes/actions/icons/mi-off.png b/dist/nodes/actions/icons/mi-off.png new file mode 100644 index 0000000..2bce1bb Binary files /dev/null and b/dist/nodes/actions/icons/mi-off.png differ diff --git a/dist/nodes/actions/icons/mi-on.png b/dist/nodes/actions/icons/mi-on.png new file mode 100644 index 0000000..ff48563 Binary files /dev/null and b/dist/nodes/actions/icons/mi-on.png differ diff --git a/dist/nodes/actions/icons/mi-read.png b/dist/nodes/actions/icons/mi-read.png new file mode 100644 index 0000000..3acc601 Binary files /dev/null and b/dist/nodes/actions/icons/mi-read.png differ diff --git a/dist/nodes/actions/icons/mi-sound.png b/dist/nodes/actions/icons/mi-sound.png new file mode 100644 index 0000000..5327f59 Binary files /dev/null and b/dist/nodes/actions/icons/mi-sound.png differ diff --git a/dist/nodes/actions/icons/mi-toggle.png b/dist/nodes/actions/icons/mi-toggle.png new file mode 100644 index 0000000..dbebc29 Binary files /dev/null and b/dist/nodes/actions/icons/mi-toggle.png differ diff --git a/dist/nodes/actions/index.html b/dist/nodes/actions/index.html new file mode 100644 index 0000000..7172968 --- /dev/null +++ b/dist/nodes/actions/index.html @@ -0,0 +1,483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dist/nodes/actions/index.js b/dist/nodes/actions/index.js new file mode 100644 index 0000000..cfb4f3b --- /dev/null +++ b/dist/nodes/actions/index.js @@ -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); + }); +}; diff --git a/dist/nodes/constants.js b/dist/nodes/constants.js new file mode 100644 index 0000000..b24b684 --- /dev/null +++ b/dist/nodes/constants.js @@ -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; diff --git a/dist/nodes/gateway-subdevices/All.js b/dist/nodes/gateway-subdevices/All.js new file mode 100644 index 0000000..5dd74c7 --- /dev/null +++ b/dist/nodes/gateway-subdevices/All.js @@ -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); +}; diff --git a/dist/nodes/gateway-subdevices/GatewaySubdevice.js b/dist/nodes/gateway-subdevices/GatewaySubdevice.js new file mode 100644 index 0000000..3fad8ea --- /dev/null +++ b/dist/nodes/gateway-subdevices/GatewaySubdevice.js @@ -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); +}; diff --git a/dist/nodes/gateway-subdevices/Plug.js b/dist/nodes/gateway-subdevices/Plug.js new file mode 100644 index 0000000..0d6d9d1 --- /dev/null +++ b/dist/nodes/gateway-subdevices/Plug.js @@ -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); +}; diff --git a/dist/nodes/gateway-subdevices/icons/door-icon.png b/dist/nodes/gateway-subdevices/icons/door-icon.png new file mode 100644 index 0000000..f33e892 Binary files /dev/null and b/dist/nodes/gateway-subdevices/icons/door-icon.png differ diff --git a/dist/nodes/gateway-subdevices/icons/mi-all.png b/dist/nodes/gateway-subdevices/icons/mi-all.png new file mode 100644 index 0000000..b2c29af Binary files /dev/null and b/dist/nodes/gateway-subdevices/icons/mi-all.png differ diff --git a/dist/nodes/gateway-subdevices/icons/mi-switch.png b/dist/nodes/gateway-subdevices/icons/mi-switch.png new file mode 100644 index 0000000..8ee44b2 Binary files /dev/null and b/dist/nodes/gateway-subdevices/icons/mi-switch.png differ diff --git a/dist/nodes/gateway-subdevices/icons/motion-icon.png b/dist/nodes/gateway-subdevices/icons/motion-icon.png new file mode 100644 index 0000000..86c7e68 Binary files /dev/null and b/dist/nodes/gateway-subdevices/icons/motion-icon.png differ diff --git a/dist/nodes/gateway-subdevices/icons/outlet-icon.png b/dist/nodes/gateway-subdevices/icons/outlet-icon.png new file mode 100644 index 0000000..f9c7e4a Binary files /dev/null and b/dist/nodes/gateway-subdevices/icons/outlet-icon.png differ diff --git a/dist/nodes/gateway-subdevices/icons/thermometer-icon.png b/dist/nodes/gateway-subdevices/icons/thermometer-icon.png new file mode 100644 index 0000000..b25be7d Binary files /dev/null and b/dist/nodes/gateway-subdevices/icons/thermometer-icon.png differ diff --git a/dist/nodes/gateway-subdevices/index.html b/dist/nodes/gateway-subdevices/index.html new file mode 100644 index 0000000..7f2db42 --- /dev/null +++ b/dist/nodes/gateway-subdevices/index.html @@ -0,0 +1,729 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dist/nodes/gateway-subdevices/index.js b/dist/nodes/gateway-subdevices/index.js new file mode 100644 index 0000000..824ec81 --- /dev/null +++ b/dist/nodes/gateway-subdevices/index.js @@ -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); + }); +}; diff --git a/dist/nodes/gateway/Gateway.js b/dist/nodes/gateway/Gateway.js new file mode 100644 index 0000000..7c40bf2 --- /dev/null +++ b/dist/nodes/gateway/Gateway.js @@ -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); +}; diff --git a/dist/nodes/gateway/GatewayConfigurator.js b/dist/nodes/gateway/GatewayConfigurator.js new file mode 100644 index 0000000..8575750 --- /dev/null +++ b/dist/nodes/gateway/GatewayConfigurator.js @@ -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 + } + } + }); +}; diff --git a/dist/nodes/gateway/GatewayIn.js b/dist/nodes/gateway/GatewayIn.js new file mode 100644 index 0000000..ab4f0d2 --- /dev/null +++ b/dist/nodes/gateway/GatewayIn.js @@ -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); +}; diff --git a/dist/nodes/gateway/GatewayOut.js b/dist/nodes/gateway/GatewayOut.js new file mode 100644 index 0000000..d60b012 --- /dev/null +++ b/dist/nodes/gateway/GatewayOut.js @@ -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); +}; diff --git a/dist/nodes/gateway/icons/mijia-io.png b/dist/nodes/gateway/icons/mijia-io.png new file mode 100644 index 0000000..7058afb Binary files /dev/null and b/dist/nodes/gateway/icons/mijia-io.png differ diff --git a/dist/nodes/gateway/icons/mijia.png b/dist/nodes/gateway/icons/mijia.png new file mode 100644 index 0000000..7f73eae Binary files /dev/null and b/dist/nodes/gateway/icons/mijia.png differ diff --git a/dist/nodes/gateway/index.html b/dist/nodes/gateway/index.html new file mode 100644 index 0000000..aba746d --- /dev/null +++ b/dist/nodes/gateway/index.html @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dist/nodes/gateway/index.js b/dist/nodes/gateway/index.js new file mode 100644 index 0000000..84df444 --- /dev/null +++ b/dist/nodes/gateway/index.js @@ -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); +}; diff --git a/dist/nodes/plug-wifi/index.html b/dist/nodes/plug-wifi/index.html new file mode 100644 index 0000000..ffcb4f9 --- /dev/null +++ b/dist/nodes/plug-wifi/index.html @@ -0,0 +1,67 @@ + + + + + diff --git a/dist/nodes/plug-wifi/index.js b/dist/nodes/plug-wifi/index.js new file mode 100644 index 0000000..1ca5165 --- /dev/null +++ b/dist/nodes/plug-wifi/index.js @@ -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); +}; diff --git a/dist/nodes/yeelight/YeelightConfigurator.js b/dist/nodes/yeelight/YeelightConfigurator.js new file mode 100644 index 0000000..644c881 --- /dev/null +++ b/dist/nodes/yeelight/YeelightConfigurator.js @@ -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 + } + } + }); +}; diff --git a/dist/nodes/yeelight/YeelightOut.js b/dist/nodes/yeelight/YeelightOut.js new file mode 100644 index 0000000..aba8d98 --- /dev/null +++ b/dist/nodes/yeelight/YeelightOut.js @@ -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; + } + } + }); + /*( 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); +}; diff --git a/dist/nodes/yeelight/icons/mi-yeelight.png b/dist/nodes/yeelight/icons/mi-yeelight.png new file mode 100644 index 0000000..0ba6ad3 Binary files /dev/null and b/dist/nodes/yeelight/icons/mi-yeelight.png differ diff --git a/dist/nodes/yeelight/index.html b/dist/nodes/yeelight/index.html new file mode 100644 index 0000000..56a9214 --- /dev/null +++ b/dist/nodes/yeelight/index.html @@ -0,0 +1,160 @@ + + + + + + + + + + + diff --git a/dist/nodes/yeelight/index.js b/dist/nodes/yeelight/index.js new file mode 100644 index 0000000..b0b659f --- /dev/null +++ b/dist/nodes/yeelight/index.js @@ -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); +}; diff --git a/dist/utils/Color.js b/dist/utils/Color.js new file mode 100644 index 0000000..a528617 --- /dev/null +++ b/dist/utils/Color.js @@ -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; diff --git a/dist/utils/index.js b/dist/utils/index.js new file mode 100644 index 0000000..a5b6e36 --- /dev/null +++ b/dist/utils/index.js @@ -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")); diff --git a/flows-overview.json b/flows-overview.json index 18d45ac..a432797 100644 --- a/flows-overview.json +++ b/flows-overview.json @@ -1 +1 @@ -[{"id":"b7132358.47476","type":"tab","label":"Mi Devices Overview","disabled":false,"info":""},{"id":"37ac569b.b3ef02","type":"xiaomi-gateway in","z":"b7132358.47476","name":"","gateway":"","ip":"","x":100,"y":220,"wires":[["b7bdc18a.4b45","94672654.872fb","b03e7c0c.d344e","540ce4ae.b5a26c","a6d2ae60.7ee088","bb36a54.ac665d8","3229d6fd.1e35fa"]]},{"id":"b7bdc18a.4b45","type":"xiaomi-ht","z":"b7132358.47476","gateway":"","name":"","sid":"","x":420,"y":140,"wires":[["37052c54.e2b62c"]]},{"id":"94672654.872fb","type":"xiaomi-magnet","z":"b7132358.47476","gateway":"","name":"","sid":"","x":440,"y":180,"wires":[["37052c54.e2b62c"]]},{"id":"b03e7c0c.d344e","type":"xiaomi-motion","z":"b7132358.47476","gateway":"","name":"","sid":"","motionmsg":"","nomotionmsg":"","output":"0","x":440,"y":220,"wires":[["37052c54.e2b62c"]]},{"id":"540ce4ae.b5a26c","type":"xiaomi-switch","z":"b7132358.47476","gateway":"","name":"","sid":"","x":440,"y":260,"wires":[["37052c54.e2b62c"]]},{"id":"a6d2ae60.7ee088","type":"xiaomi-gateway","z":"b7132358.47476","gateway":"","name":"","x":440,"y":300,"wires":[["37052c54.e2b62c"]]},{"id":"bb36a54.ac665d8","type":"xiaomi-plug","z":"b7132358.47476","gateway":"","name":"","sid":"","onmsg":"","offmsg":"","output":"0","x":430,"y":340,"wires":[["37052c54.e2b62c"]]},{"id":"37052c54.e2b62c","type":"debug","z":"b7132358.47476","name":"","active":true,"console":"false","complete":"true","x":710,"y":220,"wires":[]},{"id":"a2db7256.81c91","type":"xiaomi-ht","z":"b7132358.47476","gateway":"","name":"","sid":"","x":480,"y":560,"wires":[["ee6734f2.6e1e6"]]},{"id":"1f15867.488737a","type":"xiaomi-magnet","z":"b7132358.47476","gateway":"","name":"","sid":"","x":500,"y":600,"wires":[["ee6734f2.6e1e6"]]},{"id":"36a62693.5c11b2","type":"xiaomi-motion","z":"b7132358.47476","gateway":"","name":"","sid":"","motionmsg":"","nomotionmsg":"","output":"0","x":500,"y":640,"wires":[["ee6734f2.6e1e6"]]},{"id":"79681c1e.90ebdc","type":"xiaomi-switch","z":"b7132358.47476","gateway":"","name":"","sid":"","x":500,"y":700,"wires":[["ee6734f2.6e1e6","9e60c17d.8a64a8","6d2591a6.89d498"]]},{"id":"d5fb64b6.8e94e8","type":"xiaomi-gateway","z":"b7132358.47476","gateway":"","name":"","x":500,"y":760,"wires":[["ee6734f2.6e1e6","ff64227b.22f8a","92cbad8e.098568","520452ff.0f57e4","87e164af.8c453"]]},{"id":"6f3e29cc.ab17e","type":"inject","z":"b7132358.47476","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":700,"wires":[["a2db7256.81c91","1f15867.488737a","36a62693.5c11b2","79681c1e.90ebdc","d5fb64b6.8e94e8","3ae7a6cd.805cda","61d3e354.cc9f7c","f9105b74.d9fc5","48720e7b.df608","2e3bfe95.2f5a9a","39683881.e496b8","1e24075e.7c64a9","e3216d45.144648"]]},{"id":"3ae7a6cd.805cda","type":"xiaomi-plug","z":"b7132358.47476","gateway":"","name":"","sid":"","onmsg":"","offmsg":"","output":"0","x":490,"y":820,"wires":[["ee6734f2.6e1e6","b46eec04.6a401","53516e47.bdb338"]]},{"id":"61d3e354.cc9f7c","type":"xiaomi-plug-wifi","z":"b7132358.47476","name":"","ip":"","onmsg":"","offmsg":"","output":"0","x":500,"y":880,"wires":[[]]},{"id":"ee6734f2.6e1e6","type":"xiaomi-actions read","z":"b7132358.47476","name":"","x":810,"y":580,"wires":[["1aa456d0.dea0e9"]]},{"id":"ff64227b.22f8a","type":"xiaomi-actions get_id_list","z":"b7132358.47476","name":"","x":820,"y":800,"wires":[["1aa456d0.dea0e9"]]},{"id":"92cbad8e.098568","type":"xiaomi-actions gateway_light","z":"b7132358.47476","name":"","brightness":100,"hexRgbColor":"#ffffff","color":{"red":255,"green":255,"blue":255},"x":820,"y":840,"wires":[["1aa456d0.dea0e9"]]},{"id":"520452ff.0f57e4","type":"xiaomi-actions gateway_sound","z":"b7132358.47476","name":"","mid":"","volume":"","x":830,"y":880,"wires":[["1aa456d0.dea0e9"]]},{"id":"87e164af.8c453","type":"xiaomi-actions gateway_stop_sound","z":"b7132358.47476","name":"","x":830,"y":920,"wires":[["1aa456d0.dea0e9"]]},{"id":"b46eec04.6a401","type":"xiaomi-actions on","z":"b7132358.47476","name":"","x":820,"y":980,"wires":[["1aa456d0.dea0e9"]]},{"id":"53516e47.bdb338","type":"xiaomi-actions off","z":"b7132358.47476","name":"","x":820,"y":1020,"wires":[["1aa456d0.dea0e9"]]},{"id":"9e60c17d.8a64a8","type":"xiaomi-actions click","z":"b7132358.47476","name":"","x":810,"y":680,"wires":[["1aa456d0.dea0e9"]]},{"id":"6d2591a6.89d498","type":"xiaomi-actions double_click","z":"b7132358.47476","name":"","x":830,"y":720,"wires":[["1aa456d0.dea0e9"]]},{"id":"1aa456d0.dea0e9","type":"xiaomi-gateway out","z":"b7132358.47476","name":"","gateway":"","ip":"","x":1200,"y":780,"wires":[]},{"id":"f9105b74.d9fc5","type":"xiaomi-actions on","z":"b7132358.47476","name":"","x":280,"y":860,"wires":[["61d3e354.cc9f7c"]]},{"id":"48720e7b.df608","type":"xiaomi-actions off","z":"b7132358.47476","name":"","x":280,"y":900,"wires":[["61d3e354.cc9f7c"]]},{"id":"29bb58e2.28de","type":"comment","z":"b7132358.47476","name":"Filter incoming","info":"","x":420,"y":40,"wires":[]},{"id":"33967b37.6d4e44","type":"comment","z":"b7132358.47476","name":"Outgoing","info":"","x":420,"y":500,"wires":[]},{"id":"a39ea960.771b88","type":"xiaomi-yeelight out","z":"b7132358.47476","name":"","ip":"","port":55443,"x":490,"y":1040,"wires":[]},{"id":"2e3bfe95.2f5a9a","type":"xiaomi-actions on","z":"b7132358.47476","name":"","x":280,"y":980,"wires":[["a39ea960.771b88"]]},{"id":"39683881.e496b8","type":"xiaomi-actions off","z":"b7132358.47476","name":"","x":280,"y":1020,"wires":[["a39ea960.771b88"]]},{"id":"1e24075e.7c64a9","type":"xiaomi-actions toggle","z":"b7132358.47476","name":"","x":290,"y":1060,"wires":[["a39ea960.771b88"]]},{"id":"e3216d45.144648","type":"xiaomi-actions gateway_light","z":"b7132358.47476","name":"","brightness":100,"hexRgbColor":"#ffffff","color":{"red":255,"green":255,"blue":255},"x":280,"y":1100,"wires":[["a39ea960.771b88"]]},{"id":"3229d6fd.1e35fa","type":"xiaomi-all","z":"b7132358.47476","gateway":"","name":"","onlyModels":[],"excludedSids":[],"x":420,"y":100,"wires":[["37052c54.e2b62c"]]}] \ No newline at end of file +[{"id":"c26e5d65.2ba2f","type":"tab","label":"Mi Devices Overview","disabled":false,"info":""},{"id":"dc0246f8.77bab8","type":"debug","z":"c26e5d65.2ba2f","name":"","active":true,"console":"false","complete":"true","x":710,"y":220,"wires":[]},{"id":"9d3513ae.0cb99","type":"inject","z":"c26e5d65.2ba2f","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":98.5714340209961,"y":622.8571577072144,"wires":[["b109ba00.876218","2f4f3b9c.8a33dc","26fba522.5a10ca","c01e5a36.4cf8a","198c4c4f.4a0e2c","f173be1d.ec8e","b4ecbe6b.11bb18","cf39b156.791e68","7e7a6423.d08204"]]},{"id":"6299b44b.364bd4","type":"comment","z":"c26e5d65.2ba2f","name":"Filter incoming","info":"","x":420,"y":40,"wires":[]},{"id":"42ef97c5.c5c0d8","type":"comment","z":"c26e5d65.2ba2f","name":"Outgoing","info":"","x":418.57143211364746,"y":457.1428575515747,"wires":[]},{"id":"fc699927.573898","type":"mi-devices-gateway in","z":"c26e5d65.2ba2f","name":"","gateway":"","x":89.5,"y":227,"wires":[["20b19f26.614d9","935e31dd.7598c8","2d26be73.a95cf2","76949f24.3362f","8ff4f2d9.3bb058","30f884ad.d891f4","7f4fa8ce.a2abb"]]},{"id":"20b19f26.614d9","type":"mi-devices-all","z":"c26e5d65.2ba2f","gateway":"","name":"","onlyModels":[],"excludedSids":[],"x":426.5,"y":97,"wires":[["dc0246f8.77bab8"]]},{"id":"935e31dd.7598c8","type":"mi-devices-mi.weather","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","x":430.5,"y":138,"wires":[["dc0246f8.77bab8"]]},{"id":"2d26be73.a95cf2","type":"mi-devices-magnet","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","x":433.5,"y":179,"wires":[["dc0246f8.77bab8"]]},{"id":"76949f24.3362f","type":"mi-devices-mi.motion","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","x":429.5,"y":217,"wires":[["dc0246f8.77bab8"]]},{"id":"8ff4f2d9.3bb058","type":"mi-devices-mi.switch","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","x":427.5,"y":256,"wires":[["dc0246f8.77bab8"]]},{"id":"30f884ad.d891f4","type":"mi-devices-gateway","z":"c26e5d65.2ba2f","gateway":"","name":"","x":418.5,"y":295,"wires":[["dc0246f8.77bab8"]]},{"id":"7f4fa8ce.a2abb","type":"mi-devices-plug","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","onmsg":"","offmsg":"","output":"0","x":416.5,"y":333,"wires":[["dc0246f8.77bab8"]]},{"id":"c8141d3d.1b5248","type":"mi-devices-gateway out","z":"c26e5d65.2ba2f","name":"","x":1018.0714340209961,"y":694.8571577072144,"wires":[]},{"id":"b109ba00.876218","type":"mi-devices-mi.weather","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","x":397.0714340209961,"y":514.8571577072144,"wires":[["47c4031.d1a7f7c"]]},{"id":"47c4031.d1a7f7c","type":"mi-devices-actions read","z":"c26e5d65.2ba2f","name":"","x":678.0714340209961,"y":519.8571577072144,"wires":[["c8141d3d.1b5248"]]},{"id":"2f4f3b9c.8a33dc","type":"mi-devices-magnet","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","x":389.0714340209961,"y":552.8571577072144,"wires":[["47c4031.d1a7f7c"]]},{"id":"26fba522.5a10ca","type":"mi-devices-mi.motion","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","x":399.0714340209961,"y":593.8571577072144,"wires":[["47c4031.d1a7f7c"]]},{"id":"c01e5a36.4cf8a","type":"mi-devices-mi.switch","z":"c26e5d65.2ba2f","gateway":"","name":"","sid":"","x":399.0714340209961,"y":632.8571577072144,"wires":[["47c4031.d1a7f7c","d15c2b2e.8e4d48","d61e07d.3171b78"]]},{"id":"d15c2b2e.8e4d48","type":"mi-devices-actions click","z":"c26e5d65.2ba2f","name":"","x":673.0714340209961,"y":612.8571577072144,"wires":[["c8141d3d.1b5248"]]},{"id":"d61e07d.3171b78","type":"mi-devices-actions double_click","z":"c26e5d65.2ba2f","name":"","x":701.0714340209961,"y":649.8571577072144,"wires":[["c8141d3d.1b5248"]]},{"id":"198c4c4f.4a0e2c","type":"mi-devices-gateway","z":"c26e5d65.2ba2f","gateway":"","name":"","x":361.0714340209961,"y":706.8571577072144,"wires":[["47c4031.d1a7f7c","2d3b6309.eb4df4","bb2c9352.8b7c3","d31c9974.bcb9c8","dc7a7421.cd3ca8"]]},{"id":"2d3b6309.eb4df4","type":"mi-devices-actions get_id_list","z":"c26e5d65.2ba2f","name":"","x":692.0714340209961,"y":720.8571577072144,"wires":[["c8141d3d.1b5248"]]},{"id":"bb2c9352.8b7c3","type":"mi-devices-actions light","z":"c26e5d65.2ba2f","name":"","brightness":100,"hexRgbColor":"#ffffff","color":{"red":255,"green":255,"blue":255},"x":654.0714340209961,"y":761.8571577072144,"wires":[["c8141d3d.1b5248"]]},{"id":"d31c9974.bcb9c8","type":"mi-devices-actions gateway_play_sound","z":"c26e5d65.2ba2f","name":"","mid":"","volume":"","x":664.0714340209961,"y":801.8571577072144,"wires":[["c8141d3d.1b5248"]]},{"id":"dc7a7421.cd3ca8","type":"mi-devices-actions gateway_stop_sound","z":"c26e5d65.2ba2f","name":"","x":665.0714340209961,"y":839.8571577072144,"wires":[["c8141d3d.1b5248"]]},{"id":"629b3888.01683","type":"mi-devices-yeelight out","z":"c26e5d65.2ba2f","name":"","yeelight":"","x":725.0714340209961,"y":968.8571577072144,"wires":[]},{"id":"f173be1d.ec8e","type":"mi-devices-actions turn_on","z":"c26e5d65.2ba2f","name":"","x":390.0714340209961,"y":897.8571577072144,"wires":[["629b3888.01683"]]},{"id":"b4ecbe6b.11bb18","type":"mi-devices-actions turn_off","z":"c26e5d65.2ba2f","name":"","x":391.0714340209961,"y":938.8571577072144,"wires":[["629b3888.01683"]]},{"id":"cf39b156.791e68","type":"mi-devices-actions toggle","z":"c26e5d65.2ba2f","name":"","x":392.0714340209961,"y":981.8571577072144,"wires":[["629b3888.01683"]]},{"id":"7e7a6423.d08204","type":"mi-devices-actions light","z":"c26e5d65.2ba2f","name":"","brightness":100,"hexRgbColor":"#ffffff","color":{"red":255,"green":255,"blue":255},"x":363.0714340209961,"y":1021.8571577072144,"wires":[["629b3888.01683"]]}] \ No newline at end of file diff --git a/flows-sample.json b/flows-sample.json index 8f71180..5d35a73 100644 --- a/flows-sample.json +++ b/flows-sample.json @@ -1 +1 @@ -[{"id":"42c840ac.0d4088","type":"tab","label":"Mi Devices Sample","disabled":false,"info":""},{"id":"a2d5e2c3.b92fd","type":"comment","z":"42c840ac.0d4088","name":"Get all sensors and gateway statuses","info":"","x":390,"y":40,"wires":[]},{"id":"c53c673b.414f28","type":"inject","z":"42c840ac.0d4088","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"x":107.14285714285711,"y":95,"wires":[["41eb0695.e63408","eafddced.3b04a"]]},{"id":"41eb0695.e63408","type":"xiaomi-all","z":"42c840ac.0d4088","gateway":"","name":"","x":300,"y":100,"wires":[["91c1bb66.39253"]]},{"id":"eafddced.3b04a","type":"xiaomi-gateway","z":"42c840ac.0d4088","gateway":"","name":"","x":320,"y":160,"wires":[["d19fb980.133f68"]]},{"id":"91c1bb66.39253","type":"split","z":"42c840ac.0d4088","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":490,"y":100,"wires":[["3185cd39.74c96a"]]},{"id":"3185cd39.74c96a","type":"change","z":"42c840ac.0d4088","name":"set id","rules":[{"t":"set","p":"sid","pt":"msg","to":"payload.sid","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":650,"y":100,"wires":[["d19fb980.133f68"]]},{"id":"d19fb980.133f68","type":"xiaomi-actions read","z":"42c840ac.0d4088","name":"","x":810,"y":100,"wires":[["257a96ea.456742"]]},{"id":"257a96ea.456742","type":"xiaomi-gateway out","z":"42c840ac.0d4088","name":"","gateway":"","ip":"","x":1000,"y":100,"wires":[]},{"id":"15a7a55c.ee2a13","type":"comment","z":"42c840ac.0d4088","name":"Check if a window at least one window open","info":"","x":410,"y":300,"wires":[]},{"id":"6444fdcc.e8d0b4","type":"xiaomi-all","z":"42c840ac.0d4088","gateway":"","name":"","x":300,"y":360,"wires":[["f3ac9439.133d68","47f2046.6bea47c"]]},{"id":"c04374e6.58de3","type":"split","z":"42c840ac.0d4088","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":630,"y":360,"wires":[["97dc0a89.8f2808"]]},{"id":"f3ac9439.133d68","type":"function","z":"42c840ac.0d4088","name":"filter windows","func":"let windowSensors = msg.payload.filter((e) => {\n return e.model === \"magnet\";\n});\nmsg.payload = windowSensors;\nreturn msg;","outputs":1,"noerr":0,"x":460,"y":360,"wires":[["c04374e6.58de3"]]},{"id":"97dc0a89.8f2808","type":"change","z":"42c840ac.0d4088","name":"set id","rules":[{"t":"set","p":"sid","pt":"msg","to":"payload.sid","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":770,"y":360,"wires":[["c92e4522.5f451"]]},{"id":"c92e4522.5f451","type":"xiaomi-actions read","z":"42c840ac.0d4088","name":"","x":910,"y":360,"wires":[["faa7cd47.2f0ed"]]},{"id":"faa7cd47.2f0ed","type":"xiaomi-gateway out","z":"42c840ac.0d4088","name":"","gateway":"","ip":"","x":1100,"y":360,"wires":[]},{"id":"a354a2db.a472c8","type":"xiaomi-gateway in","z":"42c840ac.0d4088","name":"","gateway":"","ip":"","x":100,"y":480,"wires":[["c888e01a.97754"]]},{"id":"c888e01a.97754","type":"function","z":"42c840ac.0d4088","name":"set window sensor value","func":"if ([\"magnet\", \"sensor_magnet.aq2\"].indexOf(msg.payload.model) >= 0 && msg.payload.sid !== \"158d0001ab1fa8\") {\n let globalKey = `windowSensorStatus-${msg.payload.sid}`;\n global.set(globalKey, msg.payload.data.status);\n}\n","outputs":"0","noerr":0,"x":330,"y":480,"wires":[]},{"id":"b6ce1add.fd8098","type":"function","z":"42c840ac.0d4088","name":"get window sensors values","func":"let windowSensors = {};\nmsg.payload.filter((e) => {\n return e.model === \"magnet\";\n}).forEach((e) => {\n let globalKey = `windowSensorStatus-${e.sid}`;\n let value = global.get(globalKey);\n if(!value || value == \"open\") {\n windowSensors[e.sid] = value || \"na\";\n }\n});\n\nmsg.payload = windowSensors;\nif(Object.keys(windowSensors).length) {\n return [msg, null];\n}\nreturn [null, msg];","outputs":"2","noerr":0,"x":680,"y":420,"wires":[[],[]],"outputLabels":["at least one window is open","all windows are close"]},{"id":"47f2046.6bea47c","type":"delay","z":"42c840ac.0d4088","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":450,"y":420,"wires":[["b6ce1add.fd8098"]]},{"id":"d85b4cea.a9e338","type":"comment","z":"42c840ac.0d4088","name":"Doorbell","info":"","x":300,"y":580,"wires":[]},{"id":"f1a76c90.712d08","type":"xiaomi-gateway in","z":"42c840ac.0d4088","name":"","gateway":"","ip":"","x":100,"y":660,"wires":[["2418c093.cc257"]]},{"id":"b11d36c7.eac58","type":"function","z":"42c840ac.0d4088","name":"is click","func":"if(msg.payload.cmd === \"report\" && msg.payload.data.status == \"click\") {\n return msg;\n}\nreturn null;","outputs":"1","noerr":0,"x":470,"y":660,"wires":[["f7756ac2.ebcec","b82c564e.b4d968","65bfdf1e.559d48"]]},{"id":"f7756ac2.ebcec","type":"xiaomi-actions gateway_sound","z":"42c840ac.0d4088","name":"","mid":"10","volume":"20","x":650,"y":640,"wires":[["d9796e1c.9da33"]]},{"id":"d9796e1c.9da33","type":"xiaomi-gateway out","z":"42c840ac.0d4088","name":"","gateway":"","ip":"","x":1260,"y":640,"wires":[]},{"id":"c2d7b15f.511b9","type":"template","z":"42c840ac.0d4088","name":"off","field":"brightness","fieldType":"msg","format":"handlebars","syntax":"plain","template":"0","output":"str","x":850,"y":800,"wires":[["c580b5dc.f26a8"]]},{"id":"b82c564e.b4d968","type":"template","z":"42c840ac.0d4088","name":"on","field":"brightness","fieldType":"msg","format":"handlebars","syntax":"plain","template":"100","output":"str","x":850,"y":760,"wires":[["c580b5dc.f26a8"]]},{"id":"c580b5dc.f26a8","type":"xiaomi-actions gateway_light","z":"42c840ac.0d4088","name":"","x":1000,"y":760,"wires":[["d9796e1c.9da33"]]},{"id":"65bfdf1e.559d48","type":"delay","z":"42c840ac.0d4088","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":720,"wires":[["c2d7b15f.511b9","71efc952.99aa1"]]},{"id":"71efc952.99aa1","type":"delay","z":"42c840ac.0d4088","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":760,"wires":[["b82c564e.b4d968","9de20a84.4b1798"]]},{"id":"9de20a84.4b1798","type":"delay","z":"42c840ac.0d4088","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":800,"wires":[["c2d7b15f.511b9","c6e951d9.77c5"]]},{"id":"c6e951d9.77c5","type":"delay","z":"42c840ac.0d4088","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":840,"wires":[["b82c564e.b4d968","6b737819.10c298"]]},{"id":"6b737819.10c298","type":"delay","z":"42c840ac.0d4088","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":880,"wires":[["c2d7b15f.511b9"]]},{"id":"8b00845a.d05ae","type":"comment","z":"42c840ac.0d4088","name":"gateway light flick 3 times","info":"","x":890,"y":720,"wires":[]},{"id":"2418c093.cc257","type":"xiaomi-switch","z":"42c840ac.0d4088","gateway":"","name":"","sid":"","x":300,"y":660,"wires":[["b11d36c7.eac58"]]}] \ No newline at end of file +[{"id":"a7780153.e80948","type":"tab","label":"Mi Devices Sample","disabled":false,"info":""},{"id":"e0c11cb8.d19f4","type":"comment","z":"a7780153.e80948","name":"Get all sensors and gateway statuses","info":"","x":390,"y":40,"wires":[]},{"id":"5c5e4db4.e10a84","type":"inject","z":"a7780153.e80948","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"x":107.14285714285711,"y":95,"wires":[["e6a4831a.203a","39a33c8b.2f070c"]]},{"id":"10c513e0.173424","type":"split","z":"a7780153.e80948","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":473,"y":95,"wires":[["22260d64.88cc4a"]]},{"id":"22260d64.88cc4a","type":"change","z":"a7780153.e80948","name":"set id","rules":[{"t":"set","p":"sid","pt":"msg","to":"payload.sid","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":629.9999732971191,"y":95.71428489685059,"wires":[["6958c446.7387dc"]]},{"id":"6fd23b34.f00c84","type":"comment","z":"a7780153.e80948","name":"Check if a window at least one window open","info":"","x":410,"y":300,"wires":[]},{"id":"8aad0e7d.af726","type":"split","z":"a7780153.e80948","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":630,"y":360,"wires":[["f11942f4.1940a8"]]},{"id":"cebd416e.e8ec7","type":"function","z":"a7780153.e80948","name":"filter windows","func":"let windowSensors = msg.payload.filter((e) => {\n return e.model === \"magnet\";\n});\nmsg.payload = windowSensors;\nreturn msg;","outputs":1,"noerr":0,"x":460,"y":360,"wires":[["8aad0e7d.af726"]]},{"id":"f11942f4.1940a8","type":"change","z":"a7780153.e80948","name":"set id","rules":[{"t":"set","p":"sid","pt":"msg","to":"payload.sid","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":770,"y":360,"wires":[["7ab840ba.050d1"]]},{"id":"eba1f751.2155e","type":"function","z":"a7780153.e80948","name":"set window sensor value","func":"if ([\"magnet\", \"sensor_magnet.aq2\"].indexOf(msg.payload.model) >= 0 && msg.payload.sid !== \"158d0001ab1fa8\") {\n let globalKey = `windowSensorStatus-${msg.payload.sid}`;\n global.set(globalKey, msg.payload.data.status);\n}\n","outputs":"0","noerr":0,"x":330,"y":480,"wires":[]},{"id":"fa8d5a4c.2b24","type":"function","z":"a7780153.e80948","name":"get window sensors values","func":"let windowSensors = {};\nmsg.payload.filter((e) => {\n return e.model === \"magnet\";\n}).forEach((e) => {\n let globalKey = `windowSensorStatus-${e.sid}`;\n let value = global.get(globalKey);\n if(!value || value == \"open\") {\n windowSensors[e.sid] = value || \"na\";\n }\n});\n\nmsg.payload = windowSensors;\nif(Object.keys(windowSensors).length) {\n return [msg, null];\n}\nreturn [null, msg];","outputs":"2","noerr":0,"x":680,"y":420,"wires":[[],[]],"outputLabels":["at least one window is open","all windows are close"]},{"id":"66e760b4.3aa808","type":"delay","z":"a7780153.e80948","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":450,"y":420,"wires":[["fa8d5a4c.2b24"]]},{"id":"82b6724e.158a6","type":"comment","z":"a7780153.e80948","name":"Doorbell","info":"","x":300,"y":580,"wires":[]},{"id":"b6eaa4f1.ab95e8","type":"function","z":"a7780153.e80948","name":"is click","func":"if(msg.payload.cmd === \"report\" && msg.payload.data.status == \"click\") {\n return msg;\n}\nreturn null;","outputs":"1","noerr":0,"x":470,"y":660,"wires":[["5b3e12d2.2bb634","5799ab4.8121954","53df8b56.14a174"]]},{"id":"24e5aa3d.e505be","type":"template","z":"a7780153.e80948","name":"off","field":"brightness","fieldType":"msg","format":"handlebars","syntax":"plain","template":"0","output":"str","x":850,"y":800,"wires":[["589890fb.6f3c6"]]},{"id":"5b3e12d2.2bb634","type":"template","z":"a7780153.e80948","name":"on","field":"brightness","fieldType":"msg","format":"handlebars","syntax":"plain","template":"100","output":"str","x":850,"y":760,"wires":[["589890fb.6f3c6"]]},{"id":"5799ab4.8121954","type":"delay","z":"a7780153.e80948","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":720,"wires":[["24e5aa3d.e505be","7bede046.d2ea48"]]},{"id":"7bede046.d2ea48","type":"delay","z":"a7780153.e80948","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":760,"wires":[["5b3e12d2.2bb634","cda882cb.ba62b8"]]},{"id":"cda882cb.ba62b8","type":"delay","z":"a7780153.e80948","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":800,"wires":[["24e5aa3d.e505be","50b3208b.2cd0b"]]},{"id":"50b3208b.2cd0b","type":"delay","z":"a7780153.e80948","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":840,"wires":[["5b3e12d2.2bb634","3913f36d.5871ac"]]},{"id":"3913f36d.5871ac","type":"delay","z":"a7780153.e80948","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":650,"y":880,"wires":[["24e5aa3d.e505be"]]},{"id":"4c598791.b9cef","type":"comment","z":"a7780153.e80948","name":"gateway light flick 3 times","info":"","x":890,"y":720,"wires":[]},{"id":"44f42181.0266","type":"mi-devices-gateway out","z":"a7780153.e80948","name":"","x":1047.8571853637695,"y":94.28570747375488,"wires":[]},{"id":"e6a4831a.203a","type":"mi-devices-all","z":"a7780153.e80948","gateway":"","name":"","onlyModels":[],"excludedSids":[],"x":282.14285469055176,"y":95.14285659790039,"wires":[["10c513e0.173424"]]},{"id":"39a33c8b.2f070c","type":"mi-devices-gateway","z":"a7780153.e80948","gateway":"","name":"","x":282.14286041259766,"y":145.71428203582764,"wires":[["6958c446.7387dc"]]},{"id":"6958c446.7387dc","type":"mi-devices-actions read","z":"a7780153.e80948","name":"","x":862.1428680419922,"y":94.28571510314941,"wires":[["44f42181.0266"]]},{"id":"f023637d.a64158","type":"mi-devices-gateway out","z":"a7780153.e80948","name":"","x":1128.5714988708496,"y":358.5714454650879,"wires":[]},{"id":"7ab840ba.050d1","type":"mi-devices-actions read","z":"a7780153.e80948","name":"","x":947.1428833007812,"y":360,"wires":[["f023637d.a64158"]]},{"id":"e6580bd9.860678","type":"mi-devices-all","z":"a7780153.e80948","gateway":"","name":"","onlyModels":[],"excludedSids":[],"x":239.42857360839844,"y":360.9999761581421,"wires":[["cebd416e.e8ec7","66e760b4.3aa808"]]},{"id":"27a909ad.31cbe6","type":"mi-devices-gateway in","z":"a7780153.e80948","name":"","gateway":"","x":79.28571428571428,"y":480.5714285714285,"wires":[["eba1f751.2155e"]]},{"id":"2ee4ea88.00928e","type":"mi-devices-gateway in","z":"a7780153.e80948","name":"","gateway":"","x":79.28571319580078,"y":659.9999618530273,"wires":[["38730c56.824c1c"]]},{"id":"38730c56.824c1c","type":"mi-devices-mi.switch","z":"a7780153.e80948","gateway":"","name":"","sid":"","x":282.42857360839844,"y":659.9999618530273,"wires":[["b6eaa4f1.ab95e8"]]},{"id":"53df8b56.14a174","type":"mi-devices-actions gateway_play_sound","z":"a7780153.e80948","name":"","mid":"","volume":"","x":782.1428571428571,"y":624.2857142857142,"wires":[["dbdde19e.ecc4f"]]},{"id":"589890fb.6f3c6","type":"mi-devices-actions light","z":"a7780153.e80948","name":"","brightness":100,"hexRgbColor":"#ffffff","color":{"red":255,"green":255,"blue":255},"x":1067.8571243286133,"y":775.7142715454102,"wires":[["dbdde19e.ecc4f"]]},{"id":"dbdde19e.ecc4f","type":"mi-devices-gateway out","z":"a7780153.e80948","name":"","x":1250.7144012451172,"y":652.8572006225586,"wires":[]}] \ No newline at end of file diff --git a/resources/mi-configurator.png b/resources/mi-configurator.png index 2147aec..ff2d806 100644 Binary files a/resources/mi-configurator.png and b/resources/mi-configurator.png differ diff --git a/resources/mi-devices-overview.png b/resources/mi-devices-overview.png index 01f428c..0729316 100644 Binary files a/resources/mi-devices-overview.png and b/resources/mi-devices-overview.png differ diff --git a/resources/mi-devices-sample.png b/resources/mi-devices-sample.png index 6c26fff..da3218a 100644 Binary files a/resources/mi-devices-sample.png and b/resources/mi-devices-sample.png differ