316 lines
9.9 KiB
JavaScript
316 lines
9.9 KiB
JavaScript
|
/**
|
||
|
* yunkong2 MiHome
|
||
|
*
|
||
|
* Copyright 2017-2018, bluefox <dogafox@gmail.com>
|
||
|
*
|
||
|
* License: MIT
|
||
|
*/
|
||
|
'use strict';
|
||
|
const utils = require('./lib/utils'); // Get common adapter utils
|
||
|
const MiHome = require('./lib/Hub');
|
||
|
const adapter = utils.Adapter('mihome');
|
||
|
|
||
|
let objects = {};
|
||
|
let delayed = {};
|
||
|
let connected = null;
|
||
|
let connTimeout;
|
||
|
let hub;
|
||
|
let reconnectTimeout;
|
||
|
let tasks = [];
|
||
|
|
||
|
adapter.on('ready', main);
|
||
|
|
||
|
adapter.on('stateChange', (id, state) => {
|
||
|
if (!id || !state || state.ack) {
|
||
|
return;
|
||
|
}
|
||
|
if (!objects[id]) {
|
||
|
adapter.log.warn('Unknown ID: ' + id);
|
||
|
return;
|
||
|
}
|
||
|
if (hub) {
|
||
|
const pos = id.lastIndexOf('.');
|
||
|
const channelId = id.substring(0, pos);
|
||
|
const attr = id.substring(pos + 1);
|
||
|
|
||
|
if (objects[channelId] && objects[channelId].native) {
|
||
|
const device = hub.getSensor(objects[channelId].native.sid);
|
||
|
if (device && device.Control) {
|
||
|
device.Control(attr, state.val);
|
||
|
} else {
|
||
|
adapter.log.warn('Cannot control ' + id);
|
||
|
}
|
||
|
} else {
|
||
|
adapter.log.warn('Invalid device: ' + id);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
adapter.on('unload', callback => {
|
||
|
if (hub) {
|
||
|
try {
|
||
|
hub.stop(callback);
|
||
|
} catch (e) {
|
||
|
console.error('Cannot stop: ' + e);
|
||
|
callback && callback();
|
||
|
}
|
||
|
} else if (callback) {
|
||
|
callback();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
adapter.on('message', obj => {
|
||
|
if (obj) {
|
||
|
switch (obj.command) {
|
||
|
case 'browse':
|
||
|
let browse = new MiHome.Hub({
|
||
|
port: (obj.message.port || adapter.config.port) + 1,
|
||
|
bind: obj.message.bind || '0.0.0.0',
|
||
|
browse: true
|
||
|
});
|
||
|
let result = [];
|
||
|
|
||
|
browse.on('browse', data => (result.indexOf(data.ip) === -1) && result.push(data.ip));
|
||
|
|
||
|
browse.listen();
|
||
|
setTimeout(() => {
|
||
|
browse.stop(() => {
|
||
|
browse = null;
|
||
|
if (obj.callback) adapter.sendTo(obj.from, obj.command, result, obj.callback);
|
||
|
});
|
||
|
}, 3000);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function updateStates(sid, type, data) {
|
||
|
const id = adapter.namespace + '.devices.' + type.replace('.', '_') + '_' + sid;
|
||
|
|
||
|
for (const attr in data) {
|
||
|
if (data.hasOwnProperty(attr)) {
|
||
|
if (objects[id] || objects[id + '.' + attr]) {
|
||
|
adapter.setForeignState(id + '.' + attr, data[attr], true);
|
||
|
} else {
|
||
|
delayed[id + '.' + attr] = data[attr];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function syncObjects(callback) {
|
||
|
if (!tasks || !tasks.length) {
|
||
|
callback && callback();
|
||
|
return;
|
||
|
}
|
||
|
const obj = tasks.shift();
|
||
|
adapter.getForeignObject(obj._id, (err, oObj) => {
|
||
|
if (!oObj) {
|
||
|
objects[obj._id] = obj;
|
||
|
adapter.setForeignObject(obj._id, obj, () => {
|
||
|
if (delayed[obj._id] !== undefined) {
|
||
|
adapter.setForeignState(obj._id, delayed[obj._id], true, () => {
|
||
|
delete delayed[obj._id];
|
||
|
setImmediate(syncObjects, callback);
|
||
|
})
|
||
|
} else {
|
||
|
setImmediate(syncObjects, callback);
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
let changed = false;
|
||
|
// merge info together
|
||
|
for (const a in obj.common) {
|
||
|
if (obj.common.hasOwnProperty(a) && a !== 'name' && oObj.common[a] !== obj.common[a]) {
|
||
|
changed = true;
|
||
|
oObj.common[a] = obj.common[a];
|
||
|
}
|
||
|
}
|
||
|
if (JSON.stringify(obj.native) !== JSON.stringify(oObj.native)) {
|
||
|
changed = true;
|
||
|
oObj.native = obj.native;
|
||
|
}
|
||
|
|
||
|
objects[obj._id] = oObj;
|
||
|
if (changed) {
|
||
|
adapter.setForeignObject(oObj._id, oObj, () => {
|
||
|
if (delayed[oObj._id] !== undefined) {
|
||
|
adapter.setForeignState(oObj._id, delayed[oObj._id], true, () => {
|
||
|
delete delayed[oObj._id];
|
||
|
setImmediate(syncObjects, callback);
|
||
|
})
|
||
|
} else {
|
||
|
setImmediate(syncObjects, callback);
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
if (delayed[oObj._id] !== undefined) {
|
||
|
adapter.setForeignState(oObj._id, delayed[oObj._id], true, () => {
|
||
|
delete delayed[oObj._id];
|
||
|
setImmediate(syncObjects, callback);
|
||
|
})
|
||
|
} else {
|
||
|
// init rotate position with previous value
|
||
|
if (oObj._id.match(/\.rotate_position$/)) {
|
||
|
adapter.getForeignState(oObj._id, (err, state) => {
|
||
|
if (state) {
|
||
|
const pos = oObj._id.lastIndexOf('.');
|
||
|
const channelId = oObj._id.substring(0, pos);
|
||
|
|
||
|
if (objects[channelId]) {
|
||
|
const device = hub.getSensor(objects[channelId].native.sid);
|
||
|
if (device && device.Control) {
|
||
|
device.Control('rotate_position', state.val);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
setImmediate(syncObjects, callback);
|
||
|
});
|
||
|
} else {
|
||
|
setImmediate(syncObjects, callback);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function createDevice(device, name, callback) {
|
||
|
const id = adapter.namespace + '.devices.' + device.type.replace('.', '_') + '_' + device.sid;
|
||
|
const isStartTasks = !tasks.length;
|
||
|
const dev = Object.keys(MiHome.Devices).find(id => MiHome.Devices[id].type === device.type);
|
||
|
|
||
|
if (dev) {
|
||
|
for (const attr in MiHome.Devices[dev].states) {
|
||
|
if (!MiHome.Devices[dev].states.hasOwnProperty(attr)) continue;
|
||
|
console.log('Create ' + id + '.' + attr);
|
||
|
tasks.push({
|
||
|
_id: id + '.' + attr,
|
||
|
common: MiHome.Devices[dev].states[attr],
|
||
|
type: 'state',
|
||
|
native: {}
|
||
|
});
|
||
|
}
|
||
|
} else {
|
||
|
adapter.log.error('Device ' + device.type + ' not found');
|
||
|
}
|
||
|
|
||
|
tasks.push({
|
||
|
_id: id,
|
||
|
common: {
|
||
|
name: name || (dev && dev.fullName) || device.type,
|
||
|
icon: '/icons/' + device.type.replace('.', '_') + '.png'
|
||
|
},
|
||
|
type: 'channel',
|
||
|
native: {
|
||
|
sid: device.sid,
|
||
|
type: device.type
|
||
|
}
|
||
|
});
|
||
|
|
||
|
isStartTasks && syncObjects(callback);
|
||
|
}
|
||
|
|
||
|
function readObjects(callback) {
|
||
|
adapter.getForeignObjects(adapter.namespace + '.devices.*', (err, list) => {
|
||
|
adapter.subscribeStates('devices.*');
|
||
|
objects = list;
|
||
|
callback && callback();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function disconnected () {
|
||
|
connTimeout = null;
|
||
|
if (connected) {
|
||
|
connected = false;
|
||
|
adapter.log.info(`Change connection status on timeout after ${adapter.config.heartbeatTimeout}ms: false`);
|
||
|
adapter.setState('info.connection', connected, true);
|
||
|
}
|
||
|
stopMihome();
|
||
|
}
|
||
|
|
||
|
function setConnected(conn) {
|
||
|
if (connected !== conn) {
|
||
|
connected = conn;
|
||
|
adapter.log.info('Change connection status: ' + conn);
|
||
|
adapter.setState('info.connection', connected, true);
|
||
|
}
|
||
|
|
||
|
if (conn && adapter.config.heartbeatTimeout) {
|
||
|
if (connTimeout) {
|
||
|
clearTimeout(connTimeout);
|
||
|
}
|
||
|
|
||
|
connTimeout = setTimeout(disconnected, adapter.config.heartbeatTimeout);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function stopMihome() {
|
||
|
if (hub) {
|
||
|
try {
|
||
|
hub.stop();
|
||
|
hub = null;
|
||
|
} catch (e) {
|
||
|
|
||
|
}
|
||
|
}
|
||
|
if (!reconnectTimeout) {
|
||
|
reconnectTimeout = setTimeout(startMihome, adapter.config.restartInterval);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function startMihome() {
|
||
|
reconnectTimeout = null;
|
||
|
setConnected(false);
|
||
|
if (!adapter.config.key && (!adapter.config.keys || !adapter.config.keys.find(e => e.key))) {
|
||
|
adapter.log.error('no key defined. Only read is possible');
|
||
|
}
|
||
|
|
||
|
hub = new MiHome.Hub({
|
||
|
port: adapter.config.port,
|
||
|
bind: adapter.config.bind || '0.0.0.0',
|
||
|
key: adapter.config.key,
|
||
|
keys: adapter.config.keys,
|
||
|
interval: adapter.config.interval
|
||
|
});
|
||
|
|
||
|
hub.on('message', msg => {
|
||
|
setConnected(true);
|
||
|
adapter.log.debug('RAW: ' + JSON.stringify(msg));
|
||
|
});
|
||
|
hub.on('warning', msg => adapter.log.warn(msg));
|
||
|
hub.on('error', error => {
|
||
|
adapter.log.error(error);
|
||
|
stopMihome();
|
||
|
});
|
||
|
hub.on('device', (device, name) => {
|
||
|
if (!objects[adapter.namespace + '.devices.' + device.type.replace('.', '_') + '_' + device.sid]) {
|
||
|
adapter.log.debug('NEW device: ' + device.sid + '(' + device.type + ')');
|
||
|
createDevice(device, name);
|
||
|
} else {
|
||
|
adapter.log.debug('known device: ' + device.sid + '(' + device.type + ')');
|
||
|
}
|
||
|
});
|
||
|
hub.on('data', (sid, type, data) => {
|
||
|
adapter.log.debug('data: ' + sid + '(' + type + '): ' + JSON.stringify(data));
|
||
|
updateStates(sid, type, data);
|
||
|
});
|
||
|
|
||
|
if (!connTimeout && adapter.config.heartbeatTimeout) {
|
||
|
connTimeout = setTimeout(disconnected, adapter.config.heartbeatTimeout);
|
||
|
}
|
||
|
|
||
|
hub.listen();
|
||
|
}
|
||
|
|
||
|
function main() {
|
||
|
if (adapter.config.heartbeatTimeout === undefined) {
|
||
|
adapter.config.heartbeatTimeout = 20000;
|
||
|
} else {
|
||
|
adapter.config.heartbeatTimeout = parseInt(adapter.config.heartbeatTimeout, 10) || 0;
|
||
|
}
|
||
|
adapter.config.restartInterval = parseInt(adapter.config.restartInterval, 10) || 30000;
|
||
|
|
||
|
readObjects(startMihome);
|
||
|
}
|